import * as THREE from 'three'

/**
 * Panolens.js
 * @author pchen66
 * @namespace PANOLENS
 */

var PANOLENS = { REVISION: '9' }
/*! npm.im/iphone-inline-video 2.2.2 */
var enableInlineVideo = (function () {
  'use strict'/*! npm.im/intervalometer */
  function e (e, i, n, r) { function t (n) { d = i(t, r), e(n - (a || n)), a = n } var d, a; return {start: function () { d || t(0) }, stop: function () { n(d), d = null, a = 0 }} } function i (i) { return e(i, requestAnimationFrame, cancelAnimationFrame) } function n (e, i, n) { function r (r) { n && !n(e, i) || r.stopImmediatePropagation() } return e.addEventListener(i, r), r } function r (e, i, n, r) { function t () { return n[i] } function d (e) { n[i] = e }r && d(e[i]), Object.defineProperty(e, i, {get: t, set: d}) } function t (e, i, n) { n.addEventListener(i, function () { return e.dispatchEvent(new Event(i)) }) } function d (e, i) { Promise.resolve().then(function () { e.dispatchEvent(new Event(i)) }) } function a (e) { var i = new Audio(); return t(e, 'play', i), t(e, 'playing', i), t(e, 'pause', i), i.crossOrigin = e.crossOrigin, i.src = e.src || e.currentSrc || 'data:', i } function u (e, i, n) { (m || 0) + 200 < Date.now() && (e[h] = !0, m = Date.now()), n || (e.currentTime = i), k[++T % 3] = 100 * i | 0 } function o (e) { return e.driver.currentTime >= e.video.duration } function s (e) { var i = this; i.video.readyState >= i.video.HAVE_FUTURE_DATA ? (i.hasAudio || (i.driver.currentTime = i.video.currentTime + e * i.video.playbackRate / 1e3, i.video.loop && o(i) && (i.driver.currentTime = 0)), u(i.video, i.driver.currentTime)) : i.video.networkState === i.video.NETWORK_IDLE && i.video.buffered.length === 0 && i.video.load(), i.video.ended && (delete i.video[h], i.video.pause(!0)) } function c () { var e = this, i = e[g]; if (e.webkitDisplayingFullscreen) return void e[E](); i.driver.src !== 'data:' && i.driver.src !== e.src && (u(e, 0, !0), i.driver.src = e.src), e.paused && (i.paused = !1, e.buffered.length === 0 && e.load(), i.driver.play(), i.updater.start(), i.hasAudio || (d(e, 'play'), i.video.readyState >= i.video.HAVE_ENOUGH_DATA && d(e, 'playing'))) } function v (e) { var i = this, n = i[g]; n.driver.pause(), n.updater.stop(), i.webkitDisplayingFullscreen && i[w](), n.paused && !e || (n.paused = !0, n.hasAudio || d(i, 'pause'), i.ended && !i.webkitDisplayingFullscreen && (i[h] = !0, d(i, 'ended'))) } function p (e, n) { var r = {}; e[g] = r, r.paused = !0, r.hasAudio = n, r.video = e, r.updater = i(s.bind(r)), n ? r.driver = a(e) : (e.addEventListener('canplay', function () { e.paused || d(e, 'playing') }), r.driver = {src: e.src || e.currentSrc || 'data:', muted: !0, paused: !0, pause: function () { r.driver.paused = !0 }, play: function () { r.driver.paused = !1, o(r) && u(e, 0) }, get ended () { return o(r) }}), e.addEventListener('emptied', function () { var i = !r.driver.src || r.driver.src === 'data:'; r.driver.src && r.driver.src !== e.src && (u(e, 0, !0), r.driver.src = e.src, i || !n && e.autoplay ? r.driver.play() : r.updater.stop()) }, !1), e.addEventListener('webkitbeginfullscreen', function () { e.paused ? n && r.driver.buffered.length === 0 && r.driver.load() : (e.pause(), e[E]()) }), n && (e.addEventListener('webkitendfullscreen', function () { r.driver.currentTime = e.currentTime }), e.addEventListener('seeking', function () { k.indexOf(100 * e.currentTime | 0) < 0 && (r.driver.currentTime = e.currentTime) })) } function l (e) { var i = e[h]; return delete e[h], !e.webkitDisplayingFullscreen && !i } function f (e) { var i = e[g]; e[E] = e.play, e[w] = e.pause, e.play = c, e.pause = v, r(e, 'paused', i.driver), r(e, 'muted', i.driver, !0), r(e, 'playbackRate', i.driver, !0), r(e, 'ended', i.driver), r(e, 'loop', i.driver, !0), n(e, 'seeking', function (e) { return !e.webkitDisplayingFullscreen }), n(e, 'seeked', function (e) { return !e.webkitDisplayingFullscreen }), n(e, 'timeupdate', l), n(e, 'ended', l) } function y (e, i) { if (void 0 === i && (i = {}), !e[g]) { if (!i.everywhere) { if (!b) return; if (!(i.iPad || i.ipad ? /iPhone|iPod|iPad/ : /iPhone|iPod/).test(navigator.userAgent)) return }e.pause(); var n = e.autoplay; e.autoplay = !1, p(e, !e.muted), f(e), e.classList.add('IIV'), e.muted && n && (e.play(), e.addEventListener('playing', function i () { e.autoplay = !0, e.removeEventListener('playing', i) })), /iPhone|iPod|iPad/.test(navigator.platform) || console.warn('iphone-inline-video is not guaranteed to work in emulated environments') } } var m, b = typeof document === 'object' && 'object-fit' in document.head.style && !matchMedia('(-webkit-video-playable-inline)').matches, g = 'bfred-it:iphone-inline-video', h = 'bfred-it:iphone-inline-video:event', E = 'bfred-it:iphone-inline-video:nativeplay', w = 'bfred-it:iphone-inline-video:nativepause', k = [], T = 0; return y
}())
/**
 * Tween.js - Licensed under the MIT license
 * https://github.com/tweenjs/tween.js
 * ----------------------------------------------
 *
 * See https://github.com/tweenjs/tween.js/graphs/contributors for the full list of contributors.
 * Thank you all, you're awesome!
 */

var TWEEN = TWEEN || (function () {
  var _tweens = []

  return {

    getAll: function () {
      return _tweens
    },

    removeAll: function () {
      _tweens = []
    },

    add: function (tween) {
      _tweens.push(tween)
    },

    remove: function (tween) {
      var i = _tweens.indexOf(tween)

      if (i !== -1) {
        _tweens.splice(i, 1)
      }
    },

    update: function (time, preserve) {
      if (_tweens.length === 0) {
        return false
      }

      var i = 0

      time = time !== undefined ? time : TWEEN.now()

      while (i < _tweens.length) {
        if (_tweens[i].update(time) || preserve) {
          i++
        } else {
          _tweens.splice(i, 1)
        }
      }

      return true
    }
  }
})()

// Include a performance.now polyfill.
// In node.js, use process.hrtime.
if (typeof (window) === 'undefined' && typeof (process) !== 'undefined') {
  TWEEN.now = function () {
    var time = process.hrtime()

    // Convert [seconds, nanoseconds] to milliseconds.
    return time[0] * 1000 + time[1] / 1000000
  }
}
// In a browser, use window.performance.now if it is available.
else if (typeof (window) !== 'undefined' &&
         window.performance !== undefined &&
		 window.performance.now !== undefined) {
  // This must be bound, because directly assigning this function
  // leads to an invocation exception in Chrome.
  TWEEN.now = window.performance.now.bind(window.performance)
}
// Use Date.now if it is available.
else if (Date.now !== undefined) {
  TWEEN.now = Date.now
}
// Otherwise, use 'new Date().getTime()'.
else {
  TWEEN.now = function () {
    return new Date().getTime()
  }
}

TWEEN.Tween = function (object) {
  var _object = object
  var _valuesStart = {}
  var _valuesEnd = {}
  var _valuesStartRepeat = {}
  var _duration = 1000
  var _repeat = 0
  var _repeatDelayTime
  var _yoyo = false
  var _isPlaying = false
  var _reversed = false
  var _delayTime = 0
  var _startTime = null
  var _easingFunction = TWEEN.Easing.Linear.None
  var _interpolationFunction = TWEEN.Interpolation.Linear
  var _chainedTweens = []
  var _onStartCallback = null
  var _onStartCallbackFired = false
  var _onUpdateCallback = null
  var _onCompleteCallback = null
  var _onStopCallback = null

  this.to = function (properties, duration) {
    _valuesEnd = properties

    if (duration !== undefined) {
      _duration = duration
    }

    return this
  }

  this.start = function (time) {
    TWEEN.add(this)

    _isPlaying = true

    _onStartCallbackFired = false

    _startTime = time !== undefined ? time : TWEEN.now()
    _startTime += _delayTime

    for (var property in _valuesEnd) {
      // Check if an Array was provided as property value
      if (_valuesEnd[property] instanceof Array) {
        if (_valuesEnd[property].length === 0) {
          continue
        }

        // Create a local copy of the Array with the start value at the front
        _valuesEnd[property] = [_object[property]].concat(_valuesEnd[property])
      }

      // If `to()` specifies a property that doesn't exist in the source object,
      // we should not set that property in the object
      if (_object[property] === undefined) {
        continue
      }

      // Save the starting value.
      _valuesStart[property] = _object[property]

      if ((_valuesStart[property] instanceof Array) === false) {
        _valuesStart[property] *= 1.0 // Ensures we're using numbers, not strings
      }

      _valuesStartRepeat[property] = _valuesStart[property] || 0
    }

    return this
  }

  this.stop = function () {
    if (!_isPlaying) {
      return this
    }

    TWEEN.remove(this)
    _isPlaying = false

    if (_onStopCallback !== null) {
      _onStopCallback.call(_object, _object)
    }

    this.stopChainedTweens()
    return this
  }

  this.end = function () {
    this.update(_startTime + _duration)
    return this
  }

  this.stopChainedTweens = function () {
    for (var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++) {
      _chainedTweens[i].stop()
    }
  }

  this.delay = function (amount) {
    _delayTime = amount
    return this
  }

  this.repeat = function (times) {
    _repeat = times
    return this
  }

  this.repeatDelay = function (amount) {
    _repeatDelayTime = amount
    return this
  }

  this.yoyo = function (yoyo) {
    _yoyo = yoyo
    return this
  }

  this.easing = function (easing) {
    _easingFunction = easing
    return this
  }

  this.interpolation = function (interpolation) {
    _interpolationFunction = interpolation
    return this
  }

  this.chain = function () {
    _chainedTweens = arguments
    return this
  }

  this.onStart = function (callback) {
    _onStartCallback = callback
    return this
  }

  this.onUpdate = function (callback) {
    _onUpdateCallback = callback
    return this
  }

  this.onComplete = function (callback) {
    _onCompleteCallback = callback
    return this
  }

  this.onStop = function (callback) {
    _onStopCallback = callback
    return this
  }

  this.update = function (time) {
    var property
    var elapsed
    var value

    if (time < _startTime) {
      return true
    }

    if (_onStartCallbackFired === false) {
      if (_onStartCallback !== null) {
        _onStartCallback.call(_object, _object)
      }

      _onStartCallbackFired = true
    }

    elapsed = (time - _startTime) / _duration
    elapsed = elapsed > 1 ? 1 : elapsed

    value = _easingFunction(elapsed)

    for (property in _valuesEnd) {
      // Don't update properties that do not exist in the source object
      if (_valuesStart[property] === undefined) {
        continue
      }

      var start = _valuesStart[property] || 0
      var end = _valuesEnd[property]

      if (end instanceof Array) {
        _object[property] = _interpolationFunction(end, value)
      } else {
        // Parses relative end values with start as base (e.g.: +10, -3)
        if (typeof (end) === 'string') {
          if (end.charAt(0) === '+' || end.charAt(0) === '-') {
            end = start + parseFloat(end)
          } else {
            end = parseFloat(end)
          }
        }

        // Protect against non numeric properties.
        if (typeof (end) === 'number') {
          _object[property] = start + (end - start) * value
        }
      }
    }

    if (_onUpdateCallback !== null) {
      _onUpdateCallback.call(_object, value)
    }

    if (elapsed === 1) {
      if (_repeat > 0) {
        if (isFinite(_repeat)) {
          _repeat--
        }

        // Reassign starting values, restart by making startTime = now
        for (property in _valuesStartRepeat) {
          if (typeof (_valuesEnd[property]) === 'string') {
            _valuesStartRepeat[property] = _valuesStartRepeat[property] + parseFloat(_valuesEnd[property])
          }

          if (_yoyo) {
            var tmp = _valuesStartRepeat[property]

            _valuesStartRepeat[property] = _valuesEnd[property]
            _valuesEnd[property] = tmp
          }

          _valuesStart[property] = _valuesStartRepeat[property]
        }

        if (_yoyo) {
          _reversed = !_reversed
        }

        if (_repeatDelayTime !== undefined) {
          _startTime = time + _repeatDelayTime
        } else {
          _startTime = time + _delayTime
        }

        return true
      } else {
        if (_onCompleteCallback !== null) {
          _onCompleteCallback.call(_object, _object)
        }

        for (var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++) {
          // Make the chained tweens start exactly at the time they should,
          // even if the `update()` method was called way past the duration of the tween
          _chainedTweens[i].start(_startTime + _duration)
        }

        return false
      }
    }

    return true
  }
}

TWEEN.Easing = {

  Linear: {

    None: function (k) {
      return k
    }

  },

  Quadratic: {

    In: function (k) {
      return k * k
    },

    Out: function (k) {
      return k * (2 - k)
    },

    InOut: function (k) {
      if ((k *= 2) < 1) {
        return 0.5 * k * k
      }

      return -0.5 * (--k * (k - 2) - 1)
    }

  },

  Cubic: {

    In: function (k) {
      return k * k * k
    },

    Out: function (k) {
      return --k * k * k + 1
    },

    InOut: function (k) {
      if ((k *= 2) < 1) {
        return 0.5 * k * k * k
      }

      return 0.5 * ((k -= 2) * k * k + 2)
    }

  },

  Quartic: {

    In: function (k) {
      return k * k * k * k
    },

    Out: function (k) {
      return 1 - (--k * k * k * k)
    },

    InOut: function (k) {
      if ((k *= 2) < 1) {
        return 0.5 * k * k * k * k
      }

      return -0.5 * ((k -= 2) * k * k * k - 2)
    }

  },

  Quintic: {

    In: function (k) {
      return k * k * k * k * k
    },

    Out: function (k) {
      return --k * k * k * k * k + 1
    },

    InOut: function (k) {
      if ((k *= 2) < 1) {
        return 0.5 * k * k * k * k * k
      }

      return 0.5 * ((k -= 2) * k * k * k * k + 2)
    }

  },

  Sinusoidal: {

    In: function (k) {
      return 1 - Math.cos(k * Math.PI / 2)
    },

    Out: function (k) {
      return Math.sin(k * Math.PI / 2)
    },

    InOut: function (k) {
      return 0.5 * (1 - Math.cos(Math.PI * k))
    }

  },

  Exponential: {

    In: function (k) {
      return k === 0 ? 0 : Math.pow(1024, k - 1)
    },

    Out: function (k) {
      return k === 1 ? 1 : 1 - Math.pow(2, -10 * k)
    },

    InOut: function (k) {
      if (k === 0) {
        return 0
      }

      if (k === 1) {
        return 1
      }

      if ((k *= 2) < 1) {
        return 0.5 * Math.pow(1024, k - 1)
      }

      return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2)
    }

  },

  Circular: {

    In: function (k) {
      return 1 - Math.sqrt(1 - k * k)
    },

    Out: function (k) {
      return Math.sqrt(1 - (--k * k))
    },

    InOut: function (k) {
      if ((k *= 2) < 1) {
        return -0.5 * (Math.sqrt(1 - k * k) - 1)
      }

      return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1)
    }

  },

  Elastic: {

    In: function (k) {
      if (k === 0) {
        return 0
      }

      if (k === 1) {
        return 1
      }

      return -Math.pow(2, 10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI)
    },

    Out: function (k) {
      if (k === 0) {
        return 0
      }

      if (k === 1) {
        return 1
      }

      return Math.pow(2, -10 * k) * Math.sin((k - 0.1) * 5 * Math.PI) + 1
    },

    InOut: function (k) {
      if (k === 0) {
        return 0
      }

      if (k === 1) {
        return 1
      }

      k *= 2

      if (k < 1) {
        return -0.5 * Math.pow(2, 10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI)
      }

      return 0.5 * Math.pow(2, -10 * (k - 1)) * Math.sin((k - 1.1) * 5 * Math.PI) + 1
    }

  },

  Back: {

    In: function (k) {
      var s = 1.70158

      return k * k * ((s + 1) * k - s)
    },

    Out: function (k) {
      var s = 1.70158

      return --k * k * ((s + 1) * k + s) + 1
    },

    InOut: function (k) {
      var s = 1.70158 * 1.525

      if ((k *= 2) < 1) {
        return 0.5 * (k * k * ((s + 1) * k - s))
      }

      return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2)
    }

  },

  Bounce: {

    In: function (k) {
      return 1 - TWEEN.Easing.Bounce.Out(1 - k)
    },

    Out: function (k) {
      if (k < (1 / 2.75)) {
        return 7.5625 * k * k
      } else if (k < (2 / 2.75)) {
        return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75
      } else if (k < (2.5 / 2.75)) {
        return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375
      } else {
        return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375
      }
    },

    InOut: function (k) {
      if (k < 0.5) {
        return TWEEN.Easing.Bounce.In(k * 2) * 0.5
      }

      return TWEEN.Easing.Bounce.Out(k * 2 - 1) * 0.5 + 0.5
    }

  }

}

TWEEN.Interpolation = {

  Linear: function (v, k) {
    var m = v.length - 1
    var f = m * k
    var i = Math.floor(f)
    var fn = TWEEN.Interpolation.Utils.Linear

    if (k < 0) {
      return fn(v[0], v[1], f)
    }

    if (k > 1) {
      return fn(v[m], v[m - 1], m - f)
    }

    return fn(v[i], v[i + 1 > m ? m : i + 1], f - i)
  },

  Bezier: function (v, k) {
    var b = 0
    var n = v.length - 1
    var pw = Math.pow
    var bn = TWEEN.Interpolation.Utils.Bernstein

    for (var i = 0; i <= n; i++) {
      b += pw(1 - k, n - i) * pw(k, i) * v[i] * bn(n, i)
    }

    return b
  },

  CatmullRom: function (v, k) {
    var m = v.length - 1
    var f = m * k
    var i = Math.floor(f)
    var fn = TWEEN.Interpolation.Utils.CatmullRom

    if (v[0] === v[m]) {
      if (k < 0) {
        i = Math.floor(f = m * (1 + k))
      }

      return fn(v[(i - 1 + m) % m], v[i], v[(i + 1) % m], v[(i + 2) % m], f - i)
    } else {
      if (k < 0) {
        return v[0] - (fn(v[0], v[0], v[1], v[1], -f) - v[0])
      }

      if (k > 1) {
        return v[m] - (fn(v[m], v[m], v[m - 1], v[m - 1], f - m) - v[m])
      }

      return fn(v[i ? i - 1 : 0], v[i], v[m < i + 1 ? m : i + 1], v[m < i + 2 ? m : i + 2], f - i)
    }
  },

  Utils: {

    Linear: function (p0, p1, t) {
      return (p1 - p0) * t + p0
    },

    Bernstein: function (n, i) {
      var fc = TWEEN.Interpolation.Utils.Factorial

      return fc(n) / fc(i) / fc(n - i)
    },

    Factorial: (function () {
      var a = [1]

      return function (n) {
        var s = 1

        if (a[n]) {
          return a[n]
        }

        for (var i = n; i > 1; i--) {
          s *= i
        }

        a[n] = s
        return s
      }
    })(),

    CatmullRom: function (p0, p1, p2, p3, t) {
      var v0 = (p2 - p0) * 0.5
      var v1 = (p3 - p1) * 0.5
      var t2 = t * t
      var t3 = t * t2

      return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1
    }

  }

};

// UMD (Universal Module Definition)
(function (root) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define([], function () {
      return TWEEN
    })
  } else if (typeof module !== 'undefined' && typeof exports === 'object') {
    // Node.js
    module.exports = TWEEN
  } else if (root !== undefined) {
    // Global variable
    root.TWEEN = TWEEN
  }
})(this)
/**
 * @author qiao / https://github.com/qiao
 * @author mrdoob / http://mrdoob.com
 * @author alteredq / http://alteredqualia.com/
 * @author WestLangley / http://github.com/WestLangley
 * @author erich666 / http://erichaines.com
 */
/* global THREE, console */

// This set of controls performs orbiting, dollying (zooming), and panning. It maintains
// the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is
// supported.
//
//    Orbit - left mouse / touch: one finger move
//    Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
//    Pan - right mouse, or arrow keys / touch: three finter swipe

THREE.OrbitControls = function (object, domElement) {
  this.object = object
  this.domElement = (domElement !== undefined) ? domElement : document
  this.frameId

  // API

  // Set to false to disable this control
  this.enabled = true

  // "target" sets the location of focus, where the control orbits around
  // and where it pans with respect to.
  this.target = new THREE.Vector3()

  // center is old, deprecated; use "target" instead
  this.center = this.target

  // This option actually enables dollying in and out; left as "zoom" for
  // backwards compatibility
  this.noZoom = false
  this.zoomSpeed = 1.0

  // Limits to how far you can dolly in and out ( PerspectiveCamera only )
  this.minDistance = 0
  this.maxDistance = Infinity

  // Limits to how far you can zoom in and out ( OrthographicCamera only )
  this.minZoom = 0
  this.maxZoom = Infinity

  // Set to true to disable this control
  this.noRotate = false
  this.rotateSpeed = -0.15

  // Set to true to disable this control
  this.noPan = true
  this.keyPanSpeed = 7.0	// pixels moved per arrow key push

  // Set to true to automatically rotate around the target
  this.autoRotate = false
  this.autoRotateSpeed = 2.0 // 30 seconds per round when fps is 60

  // How far you can orbit vertically, upper and lower limits.
  // Range is 0 to Math.PI radians.
  this.minPolarAngle = 0 // radians
  this.maxPolarAngle = Math.PI // radians

  // Momentum
  	this.momentumDampingFactor = 0.90
  	this.momentumScalingFactor = -0.005
  	this.momentumKeydownFactor = 20

  	// Fov
  	this.minFov = 30
  	this.maxFov = 120

  // How far you can orbit horizontally, upper and lower limits.
  // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
  this.minAzimuthAngle = -Infinity // radians
  this.maxAzimuthAngle = Infinity // radians

  // Set to true to disable use of the keys
  this.noKeys = false

  // The four arrow keys
  this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }

  // Mouse buttons
  this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }

  /// /////////
  // internals

  var scope = this

  var EPS = 10e-8
  var MEPS = 10e-5

  var rotateStart = new THREE.Vector2()
  var rotateEnd = new THREE.Vector2()
  var rotateDelta = new THREE.Vector2()

  var panStart = new THREE.Vector2()
  var panEnd = new THREE.Vector2()
  var panDelta = new THREE.Vector2()
  var panOffset = new THREE.Vector3()

  var offset = new THREE.Vector3()

  var dollyStart = new THREE.Vector2()
  var dollyEnd = new THREE.Vector2()
  var dollyDelta = new THREE.Vector2()

  var theta
  var phi
  var phiDelta = 0
  var thetaDelta = 0
  var scale = 1
  var pan = new THREE.Vector3()

  var lastPosition = new THREE.Vector3()
  var lastQuaternion = new THREE.Quaternion()

  var momentumLeft = 0, momentumUp = 0
  var eventCurrent, eventPrevious
  var momentumOn = false

  var keyUp, keyBottom, keyLeft, keyRight

  var STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 }

  var state = STATE.NONE

  // for reset

  this.target0 = this.target.clone()
  this.position0 = this.object.position.clone()
  this.zoom0 = this.object.zoom

  // so camera.up is the orbit axis

  var quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0))
  var quatInverse = quat.clone().inverse()

  // events

  var changeEvent = { type: 'change' }
  var startEvent = { type: 'start' }
  var endEvent = { type: 'end' }

  this.setLastQuaternion = function (quaternion) {
    lastQuaternion.copy(quaternion)
    scope.object.quaternion.copy(quaternion)
  }

  this.getLastPosition = function () {
    return lastPosition
  }

  this.rotateLeft = function (angle) {
    if (angle === undefined) {
      angle = getAutoRotationAngle()
    }

    thetaDelta -= angle
  }

  this.rotateUp = function (angle) {
    if (angle === undefined) {
      angle = getAutoRotationAngle()
    }

    phiDelta -= angle
  }

  // pass in distance in world space to move left
  this.panLeft = function (distance) {
    var te = this.object.matrix.elements

    // get X column of matrix
    panOffset.set(te[ 0 ], te[ 1 ], te[ 2 ])
    panOffset.multiplyScalar(-distance)

    pan.add(panOffset)
  }

  // pass in distance in world space to move up
  this.panUp = function (distance) {
    var te = this.object.matrix.elements

    // get Y column of matrix
    panOffset.set(te[ 4 ], te[ 5 ], te[ 6 ])
    panOffset.multiplyScalar(distance)

    pan.add(panOffset)
  }

  // pass in x,y of change desired in pixel space,
  // right and down are positive
  this.pan = function (deltaX, deltaY) {
    var element = scope.domElement === document ? scope.domElement.body : scope.domElement

    if (scope.object instanceof THREE.PerspectiveCamera) {
      // perspective
      var position = scope.object.position
      var offset = position.clone().sub(scope.target)
      var targetDistance = offset.length()

      // half of the fov is center to top of screen
      targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0)

      // we actually don't use screenWidth, since perspective camera is fixed to screen height
      scope.panLeft(2 * deltaX * targetDistance / element.clientHeight)
      scope.panUp(2 * deltaY * targetDistance / element.clientHeight)
    } else if (scope.object instanceof THREE.OrthographicCamera) {
      // orthographic
      scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth)
      scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight)
    } else {
      // camera neither orthographic or perspective
      console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.')
    }
  }

  this.momentum = function () {
    if (!momentumOn) return

    if (Math.abs(momentumLeft) < MEPS && Math.abs(momentumUp) < MEPS) {
      momentumOn = false
      return
    }

    momentumUp *= this.momentumDampingFactor
    momentumLeft *= this.momentumDampingFactor

    thetaDelta -= this.momentumScalingFactor * momentumLeft
    phiDelta -= this.momentumScalingFactor * momentumUp
  }

  this.dollyIn = function (dollyScale) {
    if (dollyScale === undefined) {
      dollyScale = getZoomScale()
    }

    if (scope.object instanceof THREE.PerspectiveCamera) {
      scale /= dollyScale
    } else if (scope.object instanceof THREE.OrthographicCamera) {
      scope.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom * dollyScale))
      scope.object.updateProjectionMatrix()
      scope.dispatchEvent(changeEvent)
    } else {
      console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.')
    }
  }

  this.dollyOut = function (dollyScale) {
    if (dollyScale === undefined) {
      dollyScale = getZoomScale()
    }

    if (scope.object instanceof THREE.PerspectiveCamera) {
      scale *= dollyScale
    } else if (scope.object instanceof THREE.OrthographicCamera) {
      scope.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom / dollyScale))
      scope.object.updateProjectionMatrix()
      scope.dispatchEvent(changeEvent)
    } else {
      console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.')
    }
  }

  this.update = function (ignoreUpdate) {
    var position = this.object.position

    offset.copy(position).sub(this.target)

    // rotate offset to "y-axis-is-up" space
    offset.applyQuaternion(quat)

    // angle from z-axis around y-axis

    theta = Math.atan2(offset.x, offset.z)

    // angle from y-axis

    phi = Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y)

    if (this.autoRotate && state === STATE.NONE) {
      this.rotateLeft(getAutoRotationAngle())
    }

    this.momentum()

    theta += thetaDelta
    phi += phiDelta

    // restrict theta to be between desired limits
    theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, theta))

    // restrict phi to be between desired limits
    phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi))

    // restrict phi to be betwee EPS and PI-EPS
    phi = Math.max(EPS, Math.min(Math.PI - EPS, phi))

    var radius = offset.length() * scale

    // restrict radius to be between desired limits
    radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius))

    // move target to panned location
    this.target.add(pan)

    offset.x = radius * Math.sin(phi) * Math.sin(theta)
    offset.y = radius * Math.cos(phi)
    offset.z = radius * Math.sin(phi) * Math.cos(theta)

    // rotate offset back to "camera-up-vector-is-up" space
    offset.applyQuaternion(quatInverse)

    position.copy(this.target).add(offset)

    this.object.lookAt(this.target)

    thetaDelta = 0
    phiDelta = 0
    scale = 1
    pan.set(0, 0, 0)

    // update condition is:
    // min(camera displacement, camera rotation in radians)^2 > EPS
    // using small-angle approximation cos(x/2) = 1 - x^2 / 8
    if (lastPosition.distanceToSquared(this.object.position) > EPS ||
		    8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) {
      ignoreUpdate !== true && this.dispatchEvent(changeEvent)

      lastPosition.copy(this.object.position)
      lastQuaternion.copy(this.object.quaternion)
    }
  }

  this.reset = function () {
    state = STATE.NONE

    this.target.copy(this.target0)
    this.object.position.copy(this.position0)
    this.object.zoom = this.zoom0

    this.object.updateProjectionMatrix()
    this.dispatchEvent(changeEvent)

    this.update()
  }

  this.getPolarAngle = function () {
    return phi
  }

  this.getAzimuthalAngle = function () {
    return theta
  }

  function getAutoRotationAngle () {
    return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed
  }

  function getZoomScale () {
    return Math.pow(0.95, scope.zoomSpeed)
  }

  function onMouseDown (event) {
    momentumOn = false

   		momentumLeft = momentumUp = 0

    if (scope.enabled === false) return
    event.preventDefault()

    if (event.button === scope.mouseButtons.ORBIT) {
      if (scope.noRotate === true) return

      state = STATE.ROTATE

      rotateStart.set(event.clientX, event.clientY)
    } else if (event.button === scope.mouseButtons.ZOOM) {
      if (scope.noZoom === true) return

      state = STATE.DOLLY

      dollyStart.set(event.clientX, event.clientY)
    } else if (event.button === scope.mouseButtons.PAN) {
      if (scope.noPan === true) return

      state = STATE.PAN

      panStart.set(event.clientX, event.clientY)
    }

    if (state !== STATE.NONE) {
      document.addEventListener('mousemove', onMouseMove, false)
      document.addEventListener('mouseup', onMouseUp, false)
      scope.dispatchEvent(startEvent)
    }

    scope.update()
  }

  function onMouseMove (event) {
    if (scope.enabled === false) return

    event.preventDefault()

    var element = scope.domElement === document ? scope.domElement.body : scope.domElement

    if (state === STATE.ROTATE) {
      if (scope.noRotate === true) return

      rotateEnd.set(event.clientX, event.clientY)
      rotateDelta.subVectors(rotateEnd, rotateStart)

      // rotating across whole screen goes 360 degrees around
      scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed)

      // rotating up and down along whole screen attempts to go 360, but limited to 180
      scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed)

      rotateStart.copy(rotateEnd)

      if (eventPrevious) {
        momentumLeft = event.clientX - eventPrevious.clientX
        momentumUp = event.clientY - eventPrevious.clientY
      }

      eventPrevious = event
    } else if (state === STATE.DOLLY) {
      if (scope.noZoom === true) return

      dollyEnd.set(event.clientX, event.clientY)
      dollyDelta.subVectors(dollyEnd, dollyStart)

      if (dollyDelta.y > 0) {
        scope.dollyIn()
      } else if (dollyDelta.y < 0) {
        scope.dollyOut()
      }

      dollyStart.copy(dollyEnd)
    } else if (state === STATE.PAN) {
      if (scope.noPan === true) return

      panEnd.set(event.clientX, event.clientY)
      panDelta.subVectors(panEnd, panStart)

      scope.pan(panDelta.x, panDelta.y)

      panStart.copy(panEnd)
    }

    if (state !== STATE.NONE) scope.update()
  }

  function onMouseUp (/* event */) {
    momentumOn = true

    eventPrevious = undefined

    if (scope.enabled === false) return

    document.removeEventListener('mousemove', onMouseMove, false)
    document.removeEventListener('mouseup', onMouseUp, false)
    scope.dispatchEvent(endEvent)
    state = STATE.NONE
  }

  function onMouseWheel (event) {
    if (scope.enabled === false || scope.noZoom === true || state !== STATE.NONE) return

    event.preventDefault()
    event.stopPropagation()

    var delta = 0

    if (event.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9
      delta = event.wheelDelta
    } else if (event.detail !== undefined) { // Firefox
      delta = -event.detail
    }

    if (delta > 0) {
      // scope.dollyOut();
      scope.object.fov = (scope.object.fov < scope.maxFov)
        ? scope.object.fov + 1
        : scope.maxFov
      scope.object.updateProjectionMatrix()
    } else if (delta < 0) {
      // scope.dollyIn();
      scope.object.fov = (scope.object.fov > scope.minFov)
        ? scope.object.fov - 1
        : scope.minFov
      scope.object.updateProjectionMatrix()
    }

    scope.update()
    scope.dispatchEvent(changeEvent)
    scope.dispatchEvent(startEvent)
    scope.dispatchEvent(endEvent)
  }

  function onKeyUp (event) {
    switch (event.keyCode) {
      case scope.keys.UP:
        keyUp = false
        break

      case scope.keys.BOTTOM:
        keyBottom = false
        break

      case scope.keys.LEFT:
        keyLeft = false
        break

      case scope.keys.RIGHT:
        keyRight = false
        break
    }
  }

  function onKeyDown (event) {
    if (scope.enabled === false || scope.noKeys === true || scope.noRotate === true) return

    switch (event.keyCode) {
      case scope.keys.UP:
        keyUp = true
        break

      case scope.keys.BOTTOM:
        keyBottom = true
        break

      case scope.keys.LEFT:
        keyLeft = true
        break

      case scope.keys.RIGHT:
        keyRight = true
        break
    }

    if (keyUp || keyBottom || keyLeft || keyRight) {
      momentumOn = true

      if (keyUp) momentumUp = -scope.rotateSpeed * scope.momentumKeydownFactor
      if (keyBottom) momentumUp = scope.rotateSpeed * scope.momentumKeydownFactor
      if (keyLeft) momentumLeft = -scope.rotateSpeed * scope.momentumKeydownFactor
      if (keyRight) momentumLeft = scope.rotateSpeed * scope.momentumKeydownFactor
    }
  }

  function touchstart (event) {
    momentumOn = false

    momentumLeft = momentumUp = 0

    if (scope.enabled === false) return

    switch (event.touches.length) {
      case 1:	// one-fingered touch: rotate

        if (scope.noRotate === true) return

        state = STATE.TOUCH_ROTATE

        rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY)
        break

      case 2:	// two-fingered touch: dolly

        if (scope.noZoom === true) return

        state = STATE.TOUCH_DOLLY

        var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX
        var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY
        var distance = Math.sqrt(dx * dx + dy * dy)

        break

      case 3: // three-fingered touch: pan

        if (scope.noPan === true) return

        state = STATE.TOUCH_PAN

        panStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY)
        break

      default:

        state = STATE.NONE
    }

    if (state !== STATE.NONE) scope.dispatchEvent(startEvent)
  }

  function touchmove (event) {
    if (scope.enabled === false) return

    event.preventDefault()
    event.stopPropagation()

    var element = scope.domElement === document ? scope.domElement.body : scope.domElement

    switch (event.touches.length) {
      case 1: // one-fingered touch: rotate

        if (scope.noRotate === true) return
        if (state !== STATE.TOUCH_ROTATE) return

        rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY)
        rotateDelta.subVectors(rotateEnd, rotateStart)

        // rotating across whole screen goes 360 degrees around
        scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed)
        // rotating up and down along whole screen attempts to go 360, but limited to 180
        scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed)

        rotateStart.copy(rotateEnd)

        if (eventPrevious) {
          momentumLeft = event.touches[ 0 ].pageX - eventPrevious.pageX
          momentumUp = event.touches[ 0 ].pageY - eventPrevious.pageY
        }

        eventPrevious = {
          pageX: event.touches[ 0 ].pageX,
          pageY: event.touches[ 0 ].pageY
        }

        scope.update()
        break

      case 2: // two-fingered touch: dolly

        if (scope.noZoom === true) return
        if (state !== STATE.TOUCH_DOLLY) return

        var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX
        var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY
        var distance = Math.sqrt(dx * dx + dy * dy)

        if (event.scale < 1) {
          scope.object.fov = (scope.object.fov < scope.maxFov)
            ? scope.object.fov + 1
            : scope.maxFov
          scope.object.updateProjectionMatrix()
        } else if (event.scale > 1) {
          scope.object.fov = (scope.object.fov > scope.minFov)
            ? scope.object.fov - 1
            : scope.minFov
          scope.object.updateProjectionMatrix()
        }

        scope.update()
        scope.dispatchEvent(changeEvent)
        break

      case 3: // three-fingered touch: pan

        if (scope.noPan === true) return
        if (state !== STATE.TOUCH_PAN) return

        panEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY)
        panDelta.subVectors(panEnd, panStart)

        scope.pan(panDelta.x, panDelta.y)

        panStart.copy(panEnd)

        scope.update()
        break

      default:

        state = STATE.NONE
    }
  }

  function touchend (/* event */) {
    momentumOn = true

    eventPrevious = undefined

    if (scope.enabled === false) return

    scope.dispatchEvent(endEvent)
    state = STATE.NONE
  }

  // this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
  this.domElement.addEventListener('mousedown', onMouseDown, false)
  this.domElement.addEventListener('mousewheel', onMouseWheel, false)
  this.domElement.addEventListener('DOMMouseScroll', onMouseWheel, false) // firefox

  this.domElement.addEventListener('touchstart', touchstart, false)
  this.domElement.addEventListener('touchend', touchend, false)
  this.domElement.addEventListener('touchmove', touchmove, false)

  window.addEventListener('keyup', onKeyUp, false)
  window.addEventListener('keydown', onKeyDown, false)

  // force an update at start
  this.update()
}

THREE.OrbitControls.prototype = Object.create(THREE.EventDispatcher.prototype)
THREE.OrbitControls.prototype.constructor = THREE.OrbitControls/**
 * @author richt / http://richt.me
 * @author WestLangley / http://github.com/WestLangley
 *
 * W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html)
 */

THREE.DeviceOrientationControls = function (camera, domElement) {
  var scope = this
  var changeEvent = { type: 'change' }

  var rotY = 0
  var rotX = 0
  var tempX = 0
  var tempY = 0

  this.camera = camera
  this.camera.rotation.reorder('YXZ')
  this.domElement = (domElement !== undefined) ? domElement : document

  this.enabled = true

  this.deviceOrientation = {}
  this.screenOrientation = 0

  this.alpha = 0
  this.alphaOffsetAngle = 0

  var onDeviceOrientationChangeEvent = function (event) {
    scope.deviceOrientation = event
  }

  var onScreenOrientationChangeEvent = function () {
    scope.screenOrientation = window.orientation || 0
  }

  var onTouchStartEvent = function (event) {
    event.preventDefault()
    event.stopPropagation()

    tempX = event.touches[ 0 ].pageX
    tempY = event.touches[ 0 ].pageY
  }

  var onTouchMoveEvent = function (event) {
    event.preventDefault()
    event.stopPropagation()

    rotY += THREE.Math.degToRad((event.touches[ 0 ].pageX - tempX) / 4)
    rotX += THREE.Math.degToRad((tempY - event.touches[ 0 ].pageY) / 4)

    scope.updateAlphaOffsetAngle(rotY)

    tempX = event.touches[ 0 ].pageX
    tempY = event.touches[ 0 ].pageY
  }

  // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y''

  var setCameraQuaternion = function (quaternion, alpha, beta, gamma, orient) {
    var zee = new THREE.Vector3(0, 0, 1)

    var euler = new THREE.Euler()

    var q0 = new THREE.Quaternion()

    var q1 = new THREE.Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)) // - PI/2 around the x-axis

    var vectorFingerY
    var fingerQY = new THREE.Quaternion()
    var fingerQX = new THREE.Quaternion()

    if (scope.screenOrientation == 0) {
      vectorFingerY = new THREE.Vector3(1, 0, 0)
      fingerQY.setFromAxisAngle(vectorFingerY, -rotX)
    } else if (scope.screenOrientation == 180) {
      vectorFingerY = new THREE.Vector3(1, 0, 0)
      fingerQY.setFromAxisAngle(vectorFingerY, rotX)
    } else if (scope.screenOrientation == 90) {
      vectorFingerY = new THREE.Vector3(0, 1, 0)
      fingerQY.setFromAxisAngle(vectorFingerY, rotX)
    } else if (scope.screenOrientation == -90) {
      vectorFingerY = new THREE.Vector3(0, 1, 0)
      fingerQY.setFromAxisAngle(vectorFingerY, -rotX)
    }

    q1.multiply(fingerQY)
    q1.multiply(fingerQX)

    euler.set(beta, alpha, -gamma, 'YXZ') // 'ZXY' for the device, but 'YXZ' for us

    quaternion.setFromEuler(euler) // orient the device

    quaternion.multiply(q1) // camera looks out the back of the device, not the top

    quaternion.multiply(q0.setFromAxisAngle(zee, -orient)) // adjust for screen orientation
  }

  this.connect = function () {
    onScreenOrientationChangeEvent() // run once on load

    window.addEventListener('orientationchange', onScreenOrientationChangeEvent, false)
    window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent, false)
    window.addEventListener('deviceorientation', this.update.bind(this), false)

    scope.domElement.addEventListener('touchstart', onTouchStartEvent, false)
    scope.domElement.addEventListener('touchmove', onTouchMoveEvent, false)

    scope.enabled = true
  }

  this.disconnect = function () {
    window.removeEventListener('orientationchange', onScreenOrientationChangeEvent, false)
    window.removeEventListener('deviceorientation', onDeviceOrientationChangeEvent, false)
    window.removeEventListener('deviceorientation', this.update.bind(this), false)

    scope.domElement.removeEventListener('touchstart', onTouchStartEvent, false)
    scope.domElement.removeEventListener('touchmove', onTouchMoveEvent, false)

    scope.enabled = false
  }

  this.update = function (ignoreUpdate) {
    if (scope.enabled === false) return

    var alpha = scope.deviceOrientation.alpha ? THREE.Math.degToRad(scope.deviceOrientation.alpha) + this.alphaOffsetAngle : 0 // Z
    var beta = scope.deviceOrientation.beta ? THREE.Math.degToRad(scope.deviceOrientation.beta) : 0 // X'
    var gamma = scope.deviceOrientation.gamma ? THREE.Math.degToRad(scope.deviceOrientation.gamma) : 0 // Y''
    var orient = scope.screenOrientation ? THREE.Math.degToRad(scope.screenOrientation) : 0 // O

    setCameraQuaternion(scope.camera.quaternion, alpha, beta, gamma, orient)
    this.alpha = alpha

    ignoreUpdate !== true && this.dispatchEvent(changeEvent)
  }

  this.updateAlphaOffsetAngle = function (angle) {
    this.alphaOffsetAngle = angle
    this.update()
  }

  this.dispose = function () {
    this.disconnect()
  }

  this.connect()
}

THREE.DeviceOrientationControls.prototype = Object.create(THREE.EventDispatcher.prototype)
THREE.DeviceOrientationControls.prototype.constructor = THREE.DeviceOrientationControls /** The Bend modifier lets you bend the current selection up to 90 degrees about a single axis,
 * producing a uniform bend in an object's geometry.
 * You can control the angle and direction of the bend on any of three axes.
 * The geometry has to have rather large number of polygons!
 * options:
 * 	 direction - deformation direction (in local coordinates!).
 * 	 axis - deformation axis (in local coordinates!). Vector of direction and axis are perpendicular.
 * 	 angle - deformation angle.
 * @author Vildanov Almaz / alvild@gmail.com
 * The algorithm of a bend is based on the chain line cosh: y = 1/b * cosh(b*x) - 1/b. It can be used only in three.js.
 */

THREE.BendModifier = function () {

}

THREE.BendModifier.prototype = {

  constructor: THREE.BendModifier,

  set: function (direction, axis, angle) {
    this.direction = new THREE.Vector3(); this.direction.copy(direction)
    this.axis = new THREE.Vector3(); this.axis.copy(axis)
    this.angle = angle
    return this
  },

  _sign: function (a) {
    return a < 0 ? -1 : a > 0 ? 1 : 0
  },

  _cosh: function (x) {
    return (Math.exp(x) + Math.exp(-x)) / 2
  },

  _sinhInverse: function (x) {
    return Math.log(Math.abs(x) + Math.sqrt(x * x + 1))
  },

  modify: function (geometry) {
    var thirdAxis = new THREE.Vector3(); thirdAxis.crossVectors(this.direction, this.axis)

    // P - matrices of the change-of-coordinates
    var P = new THREE.Matrix4()
    P.set(thirdAxis.x, thirdAxis.y, thirdAxis.z, 0,
      this.direction.x, this.direction.y, this.direction.z, 0,
      this.axis.x, this.axis.y, this.axis.z, 0,
      0, 0, 0, 1).transpose()

    var InverseP = new THREE.Matrix4().getInverse(P)
    var newVertices = []; var oldVertices = []; var anglesBetweenOldandNewVertices = []

    var meshGeometryBoundingBoxMaxx = 0; var meshGeometryBoundingBoxMinx = 0
    var meshGeometryBoundingBoxMaxy = 0; var meshGeometryBoundingBoxMiny = 0

    for (var i = 0; i < geometry.vertices.length; i++) {
      newVertices[i] = new THREE.Vector3(); newVertices[i].copy(geometry.vertices[i]).applyMatrix4(InverseP)
      if (newVertices[i].x > meshGeometryBoundingBoxMaxx) { meshGeometryBoundingBoxMaxx = newVertices[i].x }
      if (newVertices[i].x < meshGeometryBoundingBoxMinx) { meshGeometryBoundingBoxMinx = newVertices[i].x }
      if (newVertices[i].y > meshGeometryBoundingBoxMaxy) { meshGeometryBoundingBoxMaxy = newVertices[i].y }
      if (newVertices[i].y < meshGeometryBoundingBoxMiny) { meshGeometryBoundingBoxMiny = newVertices[i].y }
    }

    var meshWidthold = meshGeometryBoundingBoxMaxx - meshGeometryBoundingBoxMinx
    var meshDepth = meshGeometryBoundingBoxMaxy - meshGeometryBoundingBoxMiny
    var ParamB = 2 * this._sinhInverse(Math.tan(this.angle)) / meshWidthold
    var oldMiddlex = (meshGeometryBoundingBoxMaxx + meshGeometryBoundingBoxMinx) / 2
    var oldMiddley = (meshGeometryBoundingBoxMaxy + meshGeometryBoundingBoxMiny) / 2

    for (var i = 0; i < geometry.vertices.length; i++) {
      oldVertices[i] = new THREE.Vector3(); oldVertices[i].copy(newVertices[i])
      newVertices[i].x = this._sign(newVertices[i].x - oldMiddlex) * 1 / ParamB * this._sinhInverse((newVertices[i].x - oldMiddlex) * ParamB)
    }

    var meshWidth = 2 / ParamB * this._sinhInverse(meshWidthold / 2 * ParamB)

    var NewParamB = 2 * this._sinhInverse(Math.tan(this.angle)) / meshWidth

    var rightEdgePos = new THREE.Vector3(meshWidth / 2, -meshDepth / 2, 0)
    rightEdgePos.y = 1 / NewParamB * this._cosh(NewParamB * rightEdgePos.x) - 1 / NewParamB - meshDepth / 2

    var bendCenter = new THREE.Vector3(0, rightEdgePos.y + rightEdgePos.x / Math.tan(this.angle), 0)

    for (var i = 0; i < geometry.vertices.length; i++) {
      var x0 = this._sign(oldVertices[i].x - oldMiddlex) * 1 / ParamB * this._sinhInverse((oldVertices[i].x - oldMiddlex) * ParamB)
      var y0 = 1 / NewParamB * this._cosh(NewParamB * x0) - 1 / NewParamB

      var k = new THREE.Vector3(bendCenter.x - x0, bendCenter.y - (y0 - meshDepth / 2), bendCenter.z).normalize()

      var Q = new THREE.Vector3()
      Q.addVectors(new THREE.Vector3(x0, y0 - meshDepth / 2, oldVertices[i].z), k.multiplyScalar(oldVertices[i].y + meshDepth / 2))
      newVertices[i].x = Q.x; newVertices[i].y = Q.y
    }

    var middle = oldMiddlex * meshWidth / meshWidthold

    for (var i = 0; i < geometry.vertices.length; i++) {
      var O = new THREE.Vector3(oldMiddlex, oldMiddley, oldVertices[i].z)
      var p = new THREE.Vector3(); p.subVectors(oldVertices[i], O)
      var q = new THREE.Vector3(); q.subVectors(newVertices[i], O)

      anglesBetweenOldandNewVertices[i] = Math.acos(1 / this._cosh(ParamB * newVertices[i].x)) * this._sign(newVertices[i].x)

      newVertices[i].x = newVertices[i].x + middle
      geometry.vertices[i].copy(newVertices[i].applyMatrix4(P))
    }

    geometry.computeFaceNormals()
    geometry.verticesNeedUpdate = true
    geometry.normalsNeedUpdate = true

    // compute Vertex Normals
    var fvNames = [ 'a', 'b', 'c', 'd' ]

    for (var f = 0, fl = geometry.faces.length; f < fl; f++) {
      var face = geometry.faces[ f ]
      if (face.vertexNormals === undefined) {
        continue
      }
      for (var v = 0, vl = face.vertexNormals.length; v < vl; v++) {
        var angle = anglesBetweenOldandNewVertices[ face[ fvNames[ v ] ] ]
        var x = this.axis.x,
          y = this.axis.y,
          z = this.axis.z

        var rotateMatrix = new THREE.Matrix3()
        rotateMatrix.set(Math.cos(angle) + (1 - Math.cos(angle)) * x * x, (1 - Math.cos(angle)) * x * y - Math.sin(angle) * z, (1 - Math.cos(angle)) * x * z + Math.sin(angle) * y,
          (1 - Math.cos(angle)) * y * x + Math.sin(angle) * z, Math.cos(angle) + (1 - Math.cos(angle)) * y * y, (1 - Math.cos(angle)) * y * z - Math.sin(angle) * x,
          (1 - Math.cos(angle)) * z * x - Math.sin(angle) * y, (1 - Math.cos(angle)) * z * y + Math.sin(angle) * x, Math.cos(angle) + (1 - Math.cos(angle)) * z * z)

        face.vertexNormals[ v ].applyMatrix3(rotateMatrix)
      }
    }
    // end compute Vertex Normals

    return this
  }
}/**
 * @author mrdoob / http://mrdoob.com/
 */

THREE.CardboardEffect = function (renderer) {
  var _camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)

  var _scene = new THREE.Scene()

  var _stereo = new THREE.StereoCamera()
  _stereo.aspect = 0.5

  var _params = { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBAFormat }

  var _renderTarget = new THREE.WebGLRenderTarget(512, 512, _params)
  _renderTarget.scissorTest = true
  _renderTarget.texture.generateMipmaps = false

  // Distortion Mesh ported from:
  // https://github.com/borismus/webvr-boilerplate/blob/master/src/distortion/barrel-distortion-fragment.js

  var distortion = new THREE.Vector2(0.441, 0.156)

  var geometry = new THREE.PlaneBufferGeometry(1, 1, 10, 20).removeAttribute('normal').toNonIndexed()

  var positions = geometry.attributes.position.array
  var uvs = geometry.attributes.uv.array

  // duplicate
  geometry.attributes.position.count *= 2
  geometry.attributes.uv.count *= 2

  var positions2 = new Float32Array(positions.length * 2)
  positions2.set(positions)
  positions2.set(positions, positions.length)

  var uvs2 = new Float32Array(uvs.length * 2)
  uvs2.set(uvs)
  uvs2.set(uvs, uvs.length)

  var vector = new THREE.Vector2()
  var length = positions.length / 3

  for (var i = 0, l = positions2.length / 3; i < l; i++) {
    vector.x = positions2[ i * 3 + 0 ]
    vector.y = positions2[ i * 3 + 1 ]

    var dot = vector.dot(vector)
    var scalar = 1.5 + (distortion.x + distortion.y * dot) * dot

    var offset = i < length ? 0 : 1

    positions2[ i * 3 + 0 ] = (vector.x / scalar) * 1.5 - 0.5 + offset
    positions2[ i * 3 + 1 ] = (vector.y / scalar) * 3.0

    uvs2[ i * 2 ] = (uvs2[ i * 2 ] + offset) * 0.5
  }

  geometry.attributes.position.array = positions2
  geometry.attributes.uv.array = uvs2

  //

  // var material = new THREE.MeshBasicMaterial( { wireframe: true } );
  var material = new THREE.MeshBasicMaterial({ map: _renderTarget.texture })
  var mesh = new THREE.Mesh(geometry, material)
  _scene.add(mesh)

  //

  this.setSize = function (width, height) {
    renderer.setSize(width, height)

    var pixelRatio = renderer.getPixelRatio()

    _renderTarget.setSize(width * pixelRatio, height * pixelRatio)
  }

  this.render = function (scene, camera) {
    scene.updateMatrixWorld()

    if (camera.parent === null) camera.updateMatrixWorld()

    _stereo.update(camera)

    var width = _renderTarget.width / 2
    var height = _renderTarget.height

    _renderTarget.scissor.set(0, 0, width, height)
    _renderTarget.viewport.set(0, 0, width, height)
    renderer.render(scene, _stereo.cameraL, _renderTarget)

    _renderTarget.scissor.set(width, 0, width, height)
    _renderTarget.viewport.set(width, 0, width, height)
    renderer.render(scene, _stereo.cameraR, _renderTarget)

    renderer.render(_scene, _camera)
  }
}/**
 * @author alteredq / http://alteredqualia.com/
 * @authod mrdoob / http://mrdoob.com/
 * @authod arodic / http://aleksandarrodic.com/
 * @authod fonserbc / http://fonserbc.github.io/
*/

THREE.StereoEffect = function (renderer) {
  var _stereo = new THREE.StereoCamera()
  _stereo.aspect = 0.5

  this.setEyeSeparation = function (eyeSep) {
    _stereo.eyeSep = eyeSep
  }

  this.setSize = function (width, height) {
    renderer.setSize(width, height)
  }

  this.render = function (scene, camera) {
    scene.updateMatrixWorld()

    if (camera.parent === null) camera.updateMatrixWorld()

    _stereo.update(camera)

    var size = renderer.getSize()

    if (renderer.autoClear) renderer.clear()
    renderer.setScissorTest(true)

    renderer.setScissor(0, 0, size.width / 2, size.height)
    renderer.setViewport(0, 0, size.width / 2, size.height)
    renderer.render(scene, _stereo.cameraL)

    renderer.setScissor(size.width / 2, 0, size.width / 2, size.height)
    renderer.setViewport(size.width / 2, 0, size.width / 2, size.height)
    renderer.render(scene, _stereo.cameraR)

    renderer.setScissorTest(false)
  }
}
var GSVPANO = GSVPANO || {}
GSVPANO.PanoLoader = function (parameters) {
  'use strict'

  var _parameters = parameters || {},
    _location,
    _zoom,
    _panoId,
    _panoClient = new google.maps.StreetViewService(),
    _count = 0,
    _total = 0,
    _canvas = [],
    _ctx = [],
    _wc = 0,
    _hc = 0,
    result = null,
    rotation = 0,
    copyright = '',
    onSizeChange = null,
    onPanoramaLoad = null

  var levelsW = [ 1, 2, 4, 7, 13, 26 ],
    levelsH = [ 1, 1, 2, 4, 7, 13 ]

  var widths = [ 416, 832, 1664, 3328, 6656, 13312 ],
    heights = [ 416, 416, 832, 1664, 3328, 6656 ]

  var gl = null
  try {
    var canvas = document.createElement('canvas')
	    gl = canvas.getContext('experimental-webgl')
	    if (gl == null) {
	        gl = canvas.getContext('webgl')
	    }
  } catch (error) {}

  var maxW = 1024,
    maxH = 1024

  if (gl) {
    var maxTexSize = Math.max(gl.getParameter(gl.MAX_TEXTURE_SIZE), 6656)
    // alert( 'MAX_TEXTURE_SIZE ' + maxTexSize );
    maxW = maxH = maxTexSize
  }

  this.setProgress = function (loaded, total) {
    if (this.onProgress) {
      this.onProgress({loaded: loaded, total: total})
    }
  }

  this.throwError = function (message) {
    if (this.onError) {
      this.onError(message)
    } else {
      console.error(message)
    }
  }

  this.adaptTextureToZoom = function () {
    var w = levelsW[ _zoom ] * 416,
      h = levelsH[ _zoom ] * 416

    w = widths[ _zoom ]
    h = heights[ _zoom ]

    _wc = Math.ceil(w / maxW)
    _hc = Math.ceil(h / maxH)

    _canvas = []; _ctx = []

    var ptr = 0
    for (var y = 0; y < _hc; y++) {
      for (var x = 0; x < _wc; x++) {
        var c = document.createElement('canvas')
        if (x < (_wc - 1)) c.width = maxW; else c.width = w - (maxW * x)
        if (y < (_hc - 1)) c.height = maxH; else c.height = h - (maxH * y)
        _canvas.push(c)
        _ctx.push(c.getContext('2d'))
        ptr++
      }
    }
  }

  this.composeFromTile = function (x, y, texture) {
    x *= 512
    y *= 512
    var px = Math.floor(x / maxW), py = Math.floor(y / maxH)

    x -= px * maxW
    y -= py * maxH

    _ctx[ py * _wc + px ].drawImage(texture, 0, 0, texture.width, texture.height, x, y, 512, 512)
    this.progress()
  }

  this.progress = function () {
    _count++

    var p = Math.round(_count * 100 / _total)
    this.setProgress(_count, _total)

    if (_count === _total) {
      this.canvas = _canvas
      this.panoId = _panoId
      this.zoom = _zoom
      if (this.onPanoramaLoad) {
        this.onPanoramaLoad(_canvas[0])
      }
    }
  }

  this.loadFromId = function (id) {
    _panoId = id
    this.composePanorama()
  }

  this.composePanorama = function () {
    this.setProgress(0, 1)

    var w = levelsW[ _zoom ],
      h = levelsH[ _zoom ],
      self = this,
      url,
      x,
      y

    _count = 0
    _total = w * h

    var self = this
    for (var y = 0; y < h; y++) {
      for (var x = 0; x < w; x++) {
        var url = 'https://geo0.ggpht.com/cbk?cb_client=maps_sv.tactile&authuser=0&hl=en&output=tile&zoom=' + _zoom + '&x=' + x + '&y=' + y + '&panoid=' + _panoId + '&nbt&fover=2';
        (function (x, y) {
          if (_parameters.useWebGL) {
            var texture = THREE.ImageUtils.loadTexture(url, null, function () {
              self.composeFromTile(x, y, texture)
            })
          } else {
            var img = new Image()
            img.addEventListener('load', function () {
              self.composeFromTile(x, y, this)
            })
            img.crossOrigin = ''
            img.src = url
          }
        })(x, y)
      }
    }
  }

  this.load = function (panoid) {
    this.loadPano(panoid)
  }

  this.loadPano = function (id) {
    var self = this
    _panoClient.getPanoramaById(id, function (result, status) {
      if (status === google.maps.StreetViewStatus.OK) {
        self.result = result
        if (self.onPanoramaData) self.onPanoramaData(result)
        // var h = google.maps.geometry.spherical.computeHeading(location, result.location.latLng);
        // rotation = (result.tiles.centerHeading - h) * Math.PI / 180.0;
        copyright = result.copyright
        self.copyright = result.copyright
        _panoId = result.location.pano
        self.location = location
        self.composePanorama()
      } else {
        if (self.onNoPanoramaData) self.onNoPanoramaData(status)
        self.throwError('Could not retrieve panorama for the following reason: ' + status)
      }
    })
  }

  this.setZoom = function (z) {
    _zoom = z
    this.adaptTextureToZoom()
  }

  this.setZoom(_parameters.zoom || 1)
}; (function () {
  'use strict'

  /**
     * Data Image Source
     * @type {String}
     */
  PANOLENS.DataImageSource = 'https://pchen66.github.io/Panolens/asset/icon/'

  /**
     * Data Image
     * @memberOf PANOLENS
     * @enum {string}
     */
  PANOLENS.DataImage = {
    Info: PANOLENS.DataImageSource + 'information.png',
    Arrow: PANOLENS.DataImageSource + 'arrow-up.png',
    FullscreenEnter: PANOLENS.DataImageSource + 'fullscreen.png',
    FullscreenLeave: PANOLENS.DataImageSource + 'fullscreen-exit.png',
    VideoPlay: PANOLENS.DataImageSource + 'video-play.png',
    VideoPause: PANOLENS.DataImageSource + 'pause.png',
    WhiteTile: PANOLENS.DataImageSource + 'tiles.png',
    ReticleIdle: PANOLENS.DataImageSource + 'reticle-idle.png',
    Setting: PANOLENS.DataImageSource + 'setting.png',
    ChevronRight: PANOLENS.DataImageSource + 'chevron-right.png',
    Check: PANOLENS.DataImageSource + 'check.png',
    ViewIndicator: PANOLENS.DataImageSource + 'view-indicator.svg',
    ReticleDwell: PANOLENS.DataImageSource + 'reticle-animation.png'
  }
})(); (function () {
  'use strict'

  /**
	 * Control Index Enum
	 * @memberOf PANOLENS
	 * @enum {number}
	 */

  PANOLENS.Controls = {

    ORBIT: 0,

    DEVICEORIENTATION: 1

  }

  /**
	 * Effect Mode Enum
	 * @memberOf PANOLENS
	 * @enum {number}
	 */
  PANOLENS.Modes = {

    /** Unknown */
    UNKNOWN: 0,

    /** Normal */
    NORMAL: 1,

    /** Google Cardboard */
    CARDBOARD: 2,

    /** Stereoscopic **/
    STEREO: 3

  }
})(); (function () {
  'use strict'

  /**
	 * Utility
	 * @namespace PANOLENS.Utils
	 * @memberOf PANOLENS
	 * @type {object}
	 */
  PANOLENS.Utils = {}

  PANOLENS.Utils.checkTouchSupported = function () {
    return window ? 'ontouchstart' in window || window.navigator.msMaxTouchPoints : false
  }
})(); (function () {
  'use strict'

  /**
	 * Image loader with progress based on {@link https://github.com/mrdoob/three.js/blob/master/src/loaders/ImageLoader.js}
	 * @memberOf PANOLENS.Utils
	 * @namespace
	 */
  PANOLENS.Utils.ImageLoader = {}

  /**
	 * Load an image with XMLHttpRequest to provide progress checking
	 * @param  {string}   url        - An image url
	 * @param  {function} onLoad     - On load callback
	 * @param  {function} onProgress - In progress callback
	 * @param  {function} onError    - On error callback
	 * @return {HTMLImageElement}    - DOM image element
	 */

  PANOLENS.Utils.ImageLoader.load = function (url, onLoad, onProgress, onError) {
    var cached, request, arrayBufferView, blob, urlCreator, image, reference

    // Reference key
    for (var iconName in PANOLENS.DataImage) {
      if (PANOLENS.DataImage.hasOwnProperty(iconName) &&
				url === PANOLENS.DataImage[ iconName ]) {
        reference = iconName
      }
    }

    // Cached
    cached = THREE.Cache.get(reference || url)

    if (cached !== undefined) {
      if (onLoad) {
        setTimeout(function () {
          if (onProgress) {
            onProgress({ loaded: 1, total: 1 })
          }

          onLoad(cached)
        }, 0)
      }

      return cached
    }

    // Construct a new XMLHttpRequest
    urlCreator = window.URL || window.webkitURL
    image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img')

    // Add to cache
    THREE.Cache.add(reference || url, image)

    function onImageLoaded () {
      urlCreator.revokeObjectURL(image.src)
      onLoad && onLoad(image)
    }

    if (url.indexOf('data:') === 0) {
      image.addEventListener('load', onImageLoaded, false)
      image.src = url
      return image
    }

    image.crossOrigin = this.crossOrigin !== undefined ? this.crossOrigin : ''

    request = new XMLHttpRequest()
    request.open('GET', url, true)
    request.responseType = 'arraybuffer'
    request.onprogress = function (event) {
		    if (event.lengthComputable) {
		      onProgress && onProgress({ loaded: event.loaded, total: event.total })
		    }
    }
    request.onloadend = function (event) {
		    arrayBufferView = new Uint8Array(this.response)
		    blob = new Blob([ arrayBufferView ])

		    image.addEventListener('load', onImageLoaded, false)
      image.src = urlCreator.createObjectURL(blob)
    }

    request.send(null)
  }

  // Enable cache
  THREE.Cache.enabled = true
})(); (function () {
  'use strict'

  /**
	 * Texture loader based on {@link https://github.com/mrdoob/three.js/blob/master/src/loaders/TextureLoader.js}
	 * @memberOf PANOLENS.Utils
	 * @namespace
	 */
  PANOLENS.Utils.TextureLoader = {}

  /**
	 * Load image texture
	 * @param  {string}   url        - An image url
	 * @param  {function} onLoad     - On load callback
	 * @param  {function} onProgress - In progress callback
	 * @param  {function} onError    - On error callback
	 * @return {THREE.Texture}   	 - Image texture
	 */
  PANOLENS.Utils.TextureLoader.load = function (url, onLoad, onProgress, onError) {
    var texture = new THREE.Texture()

    PANOLENS.Utils.ImageLoader.load(url, function (image) {
      texture.image = image
      texture.needsUpdate = true

      onLoad && onLoad(texture)
    }, onProgress, onError)

    return texture
  }
})(); (function () {
  'use strict'

  /**
	 * Cube Texture Loader based on {@link https://github.com/mrdoob/three.js/blob/master/src/loaders/CubeTextureLoader.js}
	 * @memberOf PANOLENS.Utils
	 * @namespace
	 */
  PANOLENS.Utils.CubeTextureLoader = {}

  /**
	 * Load 6 images as a cube texture
	 * @param  {array}   urls        - Array with 6 image urls
	 * @param  {function} onLoad     - On load callback
	 * @param  {function} onProgress - In progress callback
	 * @param  {function} onError    - On error callback
	 * @return {THREE.CubeTexture}   - Cube texture
	 */
  PANOLENS.Utils.CubeTextureLoader.load = function (urls, onLoad, onProgress, onError) {
    var texture, loaded, progress, all, loadings

    texture = new THREE.CubeTexture([])

    loaded = 0
    progress = {}
    all = {}

    urls.map(function (url, index) {
      PANOLENS.Utils.ImageLoader.load(url, function (image) {
        texture.images[ index ] = image

        loaded++

        if (loaded === 6) {
          texture.needsUpdate = true

          onLoad && onLoad(texture)
        }
      }, function (event) {
        progress[ index ] = { loaded: event.loaded, total: event.total }

        all.loaded = 0
        all.total = 0
        loadings = 0

        for (var i in progress) {
          loadings++
          all.loaded += progress[ i ].loaded
          all.total += progress[ i ].total
        }

        if (loadings < 6) {
          all.total = all.total / loadings * 6
        }

        onProgress && onProgress(all)
      }, onError)
    })

    return texture
  }
})()/**
 * Stereographic projection shader
 * based on http://notlion.github.io/streetview-stereographic
 * @author pchen66
 */

PANOLENS.StereographicShader = {

  uniforms: {

    'tDiffuse': { value: new THREE.Texture() },
    'resolution': { value: 1.0 },
    'transform': { value: new THREE.Matrix4() },
    'zoom': { value: 1.0 }

  },

  vertexShader: [

    'varying vec2 vUv;',

    'void main() {',

    'vUv = uv;',
    'gl_Position = vec4( position, 1.0 );',

    '}'

  ].join('\n'),

  fragmentShader: [

    'uniform sampler2D tDiffuse;',
    'uniform float resolution;',
    'uniform mat4 transform;',
    'uniform float zoom;',

    'varying vec2 vUv;',

    'const float PI = 3.141592653589793;',

    'void main(){',

    'vec2 position = -1.0 +  2.0 * vUv;',

    'position *= vec2( zoom * resolution, zoom * 0.5 );',

    'float x2y2 = position.x * position.x + position.y * position.y;',
    'vec3 sphere_pnt = vec3( 2. * position, x2y2 - 1. ) / ( x2y2 + 1. );',

    'sphere_pnt = vec3( transform * vec4( sphere_pnt, 1.0 ) );',

    'vec2 sampleUV = vec2(',
    '(atan(sphere_pnt.y, sphere_pnt.x) / PI + 1.0) * 0.5,',
    '(asin(sphere_pnt.z) / PI + 0.5)',
    ');',

    'gl_FragColor = texture2D( tDiffuse, sampleUV );',

    '}'

  ].join('\n')

}; (function () {
  'use strict'

  /**
	 * Skeleton panorama derived from THREE.Mesh
	 * @constructor
	 * @param {THREE.Geometry} geometry - The geometry for this panorama
	 * @param {THREE.Material} material - The material for this panorama
	 */
  PANOLENS.Panorama = function (geometry, material) {
    THREE.Mesh.call(this)

    this.type = 'panorama'

    this.ImageQualityLow = 1
    this.ImageQualityFair = 2
    this.ImageQualityMedium = 3
    this.ImageQualityHigh = 4
    this.ImageQualitySuperHigh = 5

    this.animationDuration = 1000

    this.defaultInfospotSize = 350

    this.container = undefined

    this.loaded = false

    this.linkedSpots = []

    this.isInfospotVisible = false

    this.linkingImageURL = undefined
    this.linkingImageScale = undefined

    this.geometry = geometry

    this.material = material
    this.material.side = THREE.DoubleSide
    this.material.visible = false

    this.scale.x *= -1

    this.infospotAnimation = new TWEEN.Tween(this).to({}, this.animationDuration / 2)

    this.addEventListener('load', this.fadeIn.bind(this))
    this.addEventListener('panolens-container', this.setContainer.bind(this))
    this.addEventListener('click', this.onClick.bind(this))

    this.setupTransitions()
  }

  PANOLENS.Panorama.prototype = Object.create(THREE.Mesh.prototype)

  PANOLENS.Panorama.prototype.constructor = PANOLENS.Panorama

  /**
	 * Adding an object
	 * To counter the scale.x = -1, it will automatically add an
	 * empty object with inverted scale on x
	 * @param {THREE.Object3D} object - The object to be added
	 */
  PANOLENS.Panorama.prototype.add = function (object) {
    var scope, invertedObject

    scope = this

    if (arguments.length > 1) {
      for (var i = 0; i < arguments.length; i++) {
        this.add(arguments[ i ])
      }

      return this
    }

    // In case of infospots
    if (object instanceof PANOLENS.Infospot) {
      invertedObject = object

      if (object.dispatchEvent) {
        this.container && object.dispatchEvent({ type: 'panolens-container', container: this.container })

        object.dispatchEvent({ type: 'panolens-infospot-focus',
          method: function (vector, duration, easing) {
            /**
		        	 * Infospot focus handler event
		        	 * @type {object}
		        	 * @event PANOLENS.Panorama#panolens-viewer-handler
		        	 * @property {string} method - Viewer function name
		        	 * @property {*} data - The argument to be passed into the method
		        	 */
		        	scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'tweenControlCenter', data: [ vector, duration, easing ] })
          } })
      }
    } else {
      // Counter scale.x = -1 effect
      invertedObject = new THREE.Object3D()
      invertedObject.scale.x = -1
      invertedObject.scalePlaceHolder = true
      invertedObject.add(object)
    }

    THREE.Object3D.prototype.add.call(this, invertedObject)
  }

  PANOLENS.Panorama.prototype.load = function () {
    this.onLoad()
  }

  /**
	 * Click event handler
	 * @param  {object} event - Click event
	 * @fires PANOLENS.Infospot#dismiss
	 */
  PANOLENS.Panorama.prototype.onClick = function (event) {
    if (event.intersects && event.intersects.length === 0) {
      this.traverse(function (object) {
        /**
				 * Dimiss event
				 * @type {object}
				 * @event PANOLENS.Infospot#dismiss
				 */
        object.dispatchEvent({ type: 'dismiss' })
      })
    }
  }

  /**
	 * Set container of this panorama
	 * @param {HTMLElement|object} data - Data with container information
	 * @fires PANOLENS.Infospot#panolens-container
	 */
  PANOLENS.Panorama.prototype.setContainer = function (data) {
    var container

    if (data instanceof HTMLElement) {
      container = data
    } else if (data && data.container) {
      container = data.container
    }

    if (container) {
      this.children.forEach(function (child) {
        if (child instanceof PANOLENS.Infospot && child.dispatchEvent) {
          /**
					 * Set container event
					 * @type {object}
					 * @event PANOLENS.Infospot#panolens-container
					 * @property {HTMLElement} container - The container of this panorama
					 */
          child.dispatchEvent({ type: 'panolens-container', container: container })
        }
      })

      this.container = container
    }
  }

  /**
	 * This will be called when panorama is loaded
	 * @fires PANOLENS.Panorama#load
	 */
  PANOLENS.Panorama.prototype.onLoad = function () {
    this.loaded = true

    /**
		 * Load panorama event
		 * @type {object}
		 * @event PANOLENS.Panorama#load
		 */
    this.dispatchEvent({ type: 'load' })
  }

  /**
	 * This will be called when panorama is in progress
	 * @fires PANOLENS.Panorama#progress
	 */
  PANOLENS.Panorama.prototype.onProgress = function (progress) {
    /**
		 * Loading panorama progress event
		 * @type {object}
		 * @event PANOLENS.Panorama#progress
	 	 * @property {object} progress - The progress object containing loaded and total amount
		 */
    this.dispatchEvent({ type: 'progress', progress: progress })
  }

  /**
	 * This will be called when panorama loading has error
	 * @fires PANOLENS.Panorama#error
	 */
  PANOLENS.Panorama.prototype.onError = function () {
    /**
		 * Loading panorama error event
		 * @type {object}
		 * @event PANOLENS.Panorama#error
		 */
    this.dispatchEvent({ type: 'error' })
  }

  /**
	 * Get zoom level based on window width
	 * @return {number} zoom level indicating image quality
	 */
  PANOLENS.Panorama.prototype.getZoomLevel = function () {
    var zoomLevel

    if (window.innerWidth <= 800) {
      zoomLevel = this.ImageQualityFair
    } else if (window.innerWidth > 800 && window.innerWidth <= 1280) {
      zoomLevel = this.ImageQualityMedium
    } else if (window.innerWidth > 1280 && window.innerWidth <= 1920) {
      zoomLevel = this.ImageQualityHigh
    } else if (window.innerWidth > 1920) {
      zoomLevel = this.ImageQualitySuperHigh
    } else {
      zoomLevel = this.ImageQualityLow
    }

    return zoomLevel
  }

  /**
	 * Update texture of a panorama
	 * @param {THREE.Texture} texture - Texture to be updated
	 */
  PANOLENS.Panorama.prototype.updateTexture = function (texture) {
    this.material.map = texture

    this.material.needsUpdate = true
  }

  /**
	 * Toggle visibility of infospots in this panorama
	 * @param  {boolean} isVisible - Visibility of infospots
	 * @param  {number} delay - Delay in milliseconds to change visibility
	 * @fires PANOLENS.Panorama#infospot-animation-complete
	 */
  PANOLENS.Panorama.prototype.toggleInfospotVisibility = function (isVisible, delay) {
    delay = (delay !== undefined) ? delay : 0

    var scope, visible

    scope = this
    visible = (isVisible !== undefined) ? isVisible : (!this.isInfospotVisible)

    this.traverse(function (object) {
      if (object instanceof PANOLENS.Infospot) {
        visible ? object.show(delay) : object.hide(delay)
      }
    })

    this.isInfospotVisible = visible

    // Animation complete event
    this.infospotAnimation.onComplete(function () {
      /**
			 * Complete toggling infospot visibility
			 * @event PANOLENS.Panorama#infospot-animation-complete
			 * @type {object}
			 */
      scope.dispatchEvent({ type: 'infospot-animation-complete', visible: visible })
    }).delay(delay).start()
  }

  /**
	 * Set image of this panorama's linking infospot
	 * @param {string} url   - Url to the image asset
	 * @param {number} scale - Scale factor of the infospot
	 */
  PANOLENS.Panorama.prototype.setLinkingImage = function (url, scale) {
    this.linkingImageURL = url
    this.linkingImageScale = scale
  }

  /**
	 * Link one-way panorama
	 * @param  {PANOLENS.Panorama} pano  - The panorama to be linked to
	 * @param  {THREE.Vector3} position - The position of infospot which navigates to the pano
	 * @param  {number} [imageScale=300] - Image scale of linked infospot
	 * @param  {string} [imageSrc=PANOLENS.DataImage.Arrow] - The image source of linked infospot
	 */
  PANOLENS.Panorama.prototype.link = function (pano, position, imageScale, imageSrc) {
    var scope = this, spot, scale, img

    this.visible = true

    if (!position) {
      console.warn('Please specify infospot position for linking')

      return
    }

    // Infospot scale
    if (imageScale !== undefined) {
      scale = imageScale
    } else if (pano.linkingImageScale !== undefined) {
      scale = pano.linkingImageScale
    } else {
      scale = 300
    }

    // Infospot image
    if (imageSrc) {
      img = imageSrc
    } else if (pano.linkingImageURL) {
      img = pano.linkingImageURL
    } else {
      img = PANOLENS.DataImage.Arrow
    }

    // Creates a new infospot
    spot = new PANOLENS.Infospot(scale, img)
    spot.position.copy(position)
    spot.toPanorama = pano
    spot.addEventListener('click', function () {
        	/**
        	 * Viewer handler event
        	 * @type {object}
        	 * @event PANOLENS.Panorama#panolens-viewer-handler
        	 * @property {string} method - Viewer function name
        	 * @property {*} data - The argument to be passed into the method
        	 */
        	scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'setPanorama', data: pano })
    })

    this.linkedSpots.push(spot)

    this.add(spot)

    this.visible = false
  }

  PANOLENS.Panorama.prototype.reset = function () {
    this.children.length = 0
  }

  PANOLENS.Panorama.prototype.setupTransitions = function () {
    this.fadeInAnimation = new TWEEN.Tween(this.material)
      .easing(TWEEN.Easing.Quartic.Out)
      .onStart(function () {
        this.visible = true
        this.material.visible = true

        /**
				 * Enter panorama fade in start event
				 * @event PANOLENS.Panorama#enter-fade-start
				 * @type {object}
				 */
        this.dispatchEvent({ type: 'enter-fade-start' })
      }.bind(this))

    this.fadeOutAnimation = new TWEEN.Tween(this.material)
      .easing(TWEEN.Easing.Quartic.Out)
      .onComplete(function () {
        this.visible = false
        this.material.visible = true

        /**
				 * Leave panorama complete event
				 * @event PANOLENS.Panorama#leave-complete
				 * @type {object}
				 */
        this.dispatchEvent({ type: 'leave-complete' })
      }.bind(this))

    this.enterTransition = new TWEEN.Tween(this)
      .easing(TWEEN.Easing.Quartic.Out)
      .onComplete(function () {
        /**
				 * Enter panorama and animation complete event
				 * @event PANOLENS.Panorama#enter-animation-complete
				 * @type {object}
				 */
        this.dispatchEvent({ type: 'enter-animation-complete' })
      }.bind(this))
      .start()

    this.leaveTransition = new TWEEN.Tween(this)
      .easing(TWEEN.Easing.Quartic.Out)
  }

  /**
	 * Start fading in animation
	 * @fires PANOLENS.Panorama#enter-fade-complete
	 */
  PANOLENS.Panorama.prototype.fadeIn = function (duration) {
    duration = duration >= 0 ? duration : this.animationDuration

    this.fadeOutAnimation.stop()
    this.fadeInAnimation
      .to({ opacity: 1 }, duration)
      .onComplete(function () {
        this.toggleInfospotVisibility(true, duration / 2)

        /**
				 * Enter panorama fade complete event
				 * @event PANOLENS.Panorama#enter-fade-complete
				 * @type {object}
				 */
        this.dispatchEvent({ type: 'enter-fade-complete' })
      }.bind(this))
      .start()
  }

  /**
	 * Start fading out animation
	 */
  PANOLENS.Panorama.prototype.fadeOut = function (duration) {
    duration = duration >= 0 ? duration : this.animationDuration

    this.fadeInAnimation.stop()
    this.fadeOutAnimation.to({ opacity: 0 }, duration).start()
  }

  /**
	 * This will be called when entering a panorama
	 * @fires PANOLENS.Panorama#enter
	 * @fires PANOLENS.Panorama#enter-animation-start
	 */
  PANOLENS.Panorama.prototype.onEnter = function () {
    var duration = this.animationDuration

    this.leaveTransition.stop()
    this.enterTransition
      .to({}, duration)
      .onStart(function () {
        /**
				 * Enter panorama and animation starting event
				 * @event PANOLENS.Panorama#enter-animation-start
				 * @type {object}
				 */
        this.dispatchEvent({ type: 'enter-animation-start' })

        if (this.loaded) {
          this.fadeIn(duration)
        } else {
          this.load()
        }
      }.bind(this))
      .start()

    /**
		 * Enter panorama event
		 * @event PANOLENS.Panorama#enter
		 * @type {object}
		 */
    this.dispatchEvent({ type: 'enter' })
  }

  /**
	 * This will be called when leaving a panorama
	 * @fires PANOLENS.Panorama#leave
	 */
  PANOLENS.Panorama.prototype.onLeave = function () {
    var duration = this.animationDuration

    this.enterTransition.stop()
    this.leaveTransition
      .to({}, duration)
      .onStart(function () {
        /**
				 * Leave panorama and animation starting event
				 * @event PANOLENS.Panorama#leave-animation-start
				 * @type {object}
				 */
        this.dispatchEvent({ type: 'leave-animation-start' })

        this.fadeOut(duration)
        this.toggleInfospotVisibility(false)
      }.bind(this))
      .start()

    /**
		 * Leave panorama event
		 * @event PANOLENS.Panorama#leave
		 * @type {object}
		 */
    this.dispatchEvent({ type: 'leave' })
  }

  /**
	 * Dispose panorama
	 */
  PANOLENS.Panorama.prototype.dispose = function () {
    /**
    	 * On panorama dispose handler
    	 * @type {object}
    	 * @event PANOLENS.Panorama#panolens-viewer-handler
    	 * @property {string} method - Viewer function name
    	 * @property {*} data - The argument to be passed into the method
    	 */
    	this.dispatchEvent({ type: 'panolens-viewer-handler', method: 'onPanoramaDispose', data: this })

    // recursive disposal on 3d objects
    function recursiveDispose (object) {
      for (var i = object.children.length - 1; i >= 0; i--) {
        recursiveDispose(object.children[i])
        object.remove(object.children[i])
      }

      if (object instanceof PANOLENS.Infospot) {
        object.dispose()
      }

      object.geometry && object.geometry.dispose()
      object.material && object.material.dispose()
    }

    recursiveDispose(this)

    if (this.parent) {
      this.parent.remove(this)
    }
  }
})(); (function () {
  'use strict'

  /**
	 * Equirectangular based image panorama
	 * @constructor
	 * @param {string} image - Image url or HTMLImageElement
	 * @param {number} [radius=5000] - Radius of panorama
	 */
  PANOLENS.ImagePanorama = function (image, radius) {
    radius = radius || 5000

    var geometry = new THREE.SphereGeometry(radius, 60, 40),
      material = new THREE.MeshBasicMaterial({ opacity: 0, transparent: true })

    PANOLENS.Panorama.call(this, geometry, material)

    this.src = image
  }

  PANOLENS.ImagePanorama.prototype = Object.create(PANOLENS.Panorama.prototype)

  PANOLENS.ImagePanorama.prototype.constructor = PANOLENS.ImagePanorama

  /**
	 * Load image asset
	 * @param  {*} src - Url or image element
	 */
  PANOLENS.ImagePanorama.prototype.load = function (src) {
    src = src || this.src

    if (!src) {
      console.warn('Image source undefined')
    } else if (typeof src === 'string') {
      PANOLENS.Utils.TextureLoader.load(src, this.onLoad.bind(this), this.onProgress.bind(this), this.onError.bind(this))
    } else if (src instanceof HTMLImageElement) {
      this.onLoad(new THREE.Texture(src))
    }
  }

  /**
	 * This will be called when image is loaded
	 * @param  {THREE.Texture} texture - Texture to be updated
	 */
  PANOLENS.ImagePanorama.prototype.onLoad = function (texture) {
    texture.minFilter = texture.magFilter = THREE.LinearFilter

    texture.needsUpdate = true

    this.updateTexture(texture)

    // Call onLoad after second frame being painted
    window.requestAnimationFrame(function () {
      window.requestAnimationFrame(function () {
        PANOLENS.Panorama.prototype.onLoad.call(this)
      }.bind(this))
    }.bind(this))
  }

  PANOLENS.ImagePanorama.prototype.reset = function () {
    PANOLENS.Panorama.prototype.reset.call(this)
  }
})(); (function () {
  'use strict'

  /**
	 * Google streetview panorama
	 *
	 * [How to get Panorama ID]{@link http://stackoverflow.com/questions/29916149/google-maps-streetview-how-to-get-panorama-id}
	 * @constructor
	 * @param {string} panoId - Panorama id from Google Streetview
	 * @param {number} [radius=5000] - The minimum radius for this panoram
	 */
  PANOLENS.GoogleStreetviewPanorama = function (panoId, radius) {
    PANOLENS.ImagePanorama.call(this, undefined, radius)

    this.panoId = panoId

    this.gsvLoader = undefined

    this.loadRequested = false

    this.setupGoogleMapAPI()
  }

  PANOLENS.GoogleStreetviewPanorama.prototype = Object.create(PANOLENS.ImagePanorama.prototype)

  PANOLENS.GoogleStreetviewPanorama.constructor = PANOLENS.GoogleStreetviewPanorama

  /**
	 * Load Google Street View by panorama id
	 * @param {string} panoId - Gogogle Street View panorama id
	 */
  PANOLENS.GoogleStreetviewPanorama.prototype.load = function (panoId) {
    this.loadRequested = true

    panoId = (panoId || this.panoId) || {}

    if (panoId && this.gsvLoader) {
      this.loadGSVLoader(panoId)
    } else {
      this.gsvLoader = {}
    }
  }

  /**
	 * Setup Google Map API
	 */
  PANOLENS.GoogleStreetviewPanorama.prototype.setupGoogleMapAPI = function () {
    var script = document.createElement('script')
    script.src = 'https://maps.googleapis.com/maps/api/js'
    script.onreadystatechange = this.setGSVLoader.bind(this)
    	script.onload = this.setGSVLoader.bind(this)

    document.getElementsByTagName('head')[0].appendChild(script)
  }

  /**
	 * Set GSV Loader
	 */
  PANOLENS.GoogleStreetviewPanorama.prototype.setGSVLoader = function () {
    this.gsvLoader = new GSVPANO.PanoLoader()

    if (this.gsvLoader === {} || this.loadRequested) {
      this.load()
    }
  }

  /**
	 * Get GSV Loader
	 * @return {object} GSV Loader instance
	 */
  PANOLENS.GoogleStreetviewPanorama.prototype.getGSVLoader = function () {
    return this.gsvLoader
  }

  /**
	 * Load GSV Loader
	 * @param  {string} panoId - Gogogle Street View panorama id
	 */
  PANOLENS.GoogleStreetviewPanorama.prototype.loadGSVLoader = function (panoId) {
    this.loadRequested = false

    this.gsvLoader.onProgress = this.onProgress.bind(this)

    this.gsvLoader.onPanoramaLoad = this.onLoad.bind(this)

    this.gsvLoader.setZoom(this.getZoomLevel())

    this.gsvLoader.load(panoId)

    this.gsvLoader.loaded = true
  }

  /**
	 * This will be called when panorama is loaded
	 * @param  {HTMLCanvasElement} canvas - Canvas where the tiles have been drawn
	 */
  PANOLENS.GoogleStreetviewPanorama.prototype.onLoad = function (canvas) {
    if (!this.gsvLoader) { return }

    PANOLENS.ImagePanorama.prototype.onLoad.call(this, new THREE.Texture(canvas))
  }

  PANOLENS.GoogleStreetviewPanorama.prototype.reset = function () {
    this.gsvLoader = undefined

    PANOLENS.ImagePanorama.prototype.reset.call(this)
  }
})(); (function () {
  'use strict'

  /**
	 * Cubemap-based panorama
	 * @constructor
	 * @param {array} images - An array of cubetexture containing six images
	 * @param {number} [edgeLength=10000] - The length of cube's edge
	 */
  PANOLENS.CubePanorama = function (images, edgeLength) {
    var shader, geometry, material

    this.images = images || []

    edgeLength = edgeLength || 10000
    shader = JSON.parse(JSON.stringify(THREE.ShaderLib[ 'cube' ]))

    geometry = new THREE.BoxGeometry(edgeLength, edgeLength, edgeLength)
    material = new THREE.ShaderMaterial({

      fragmentShader: shader.fragmentShader,
      vertexShader: shader.vertexShader,
      uniforms: shader.uniforms,
      side: THREE.BackSide

    })

    PANOLENS.Panorama.call(this, geometry, material)
  }

  PANOLENS.CubePanorama.prototype = Object.create(PANOLENS.Panorama.prototype)

  PANOLENS.CubePanorama.prototype.constructor = PANOLENS.CubePanorama

  /**
	 * Load 6 images and bind listeners
	 */
  PANOLENS.CubePanorama.prototype.load = function () {
    PANOLENS.Utils.CubeTextureLoader.load(

      this.images,

      this.onLoad.bind(this),
      this.onProgress.bind(this),
      this.onError.bind(this)

    )
  }

  /**
	 * This will be called when 6 textures are ready
	 * @param  {THREE.CubeTexture} texture - Cube texture
	 */
  PANOLENS.CubePanorama.prototype.onLoad = function (texture) {
    this.material.uniforms[ 'tCube' ].value = texture

    PANOLENS.Panorama.prototype.onLoad.call(this)
  }
})(); (function () {
  'use strict'

  /**
	 * Basic panorama with 6 faces tile images
	 * @constructor
	 * @param {number} [edgeLength=10000] - The length of cube's edge
	 */
  PANOLENS.BasicPanorama = function (edgeLength) {
    var tile = PANOLENS.DataImage.WhiteTile

    PANOLENS.CubePanorama.call(this, [ tile, tile, tile, tile, tile, tile ], edgeLength)
  }

  PANOLENS.BasicPanorama.prototype = Object.create(PANOLENS.CubePanorama.prototype)

  PANOLENS.BasicPanorama.prototype.constructor = PANOLENS.BasicPanorama
})(); (function () {
  'use strict'

  /**
	 * Video Panorama
	 * @constructor
	 * @param {string} src - Equirectangular video url
	 * @param {object} [options] - Option for video settings
	 * @param {HTMLElement} [options.videoElement] - HTML5 video element contains the video
	 * @param {boolean} [options.loop=true] - Specify if the video should loop in the end
	 * @param {boolean} [options.muted=false] - Mute the video or not
	 * @param {boolean} [options.autoplay=false] - Specify if the video should auto play
	 * @param {boolean} [options.playsinline=true] - Specify if video should play inline for iOS. If you want it to auto play inline, set both autoplay and muted options to true
	 * @param {string} [options.crossOrigin="anonymous"] - Sets the cross-origin attribute for the video, which allows for cross-origin videos in some browsers (Firefox, Chrome). Set to either "anonymous" or "use-credentials".
	 * @param {number} [radius=5000] - The minimum radius for this panoram
	 */
  PANOLENS.VideoPanorama = function (src, options, radius) {
    radius = radius || 5000

    var geometry = new THREE.SphereGeometry(radius, 60, 40),
      material = new THREE.MeshBasicMaterial({ opacity: 0, transparent: true })

    PANOLENS.Panorama.call(this, geometry, material)

    this.src = src
    this.options = options || {}
    this.options.playsinline = this.options.playsinline !== false

    this.videoElement = undefined
    this.videoRenderObject = undefined
    this.videoProgress = 0

    this.isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent)
    this.isMobile = this.isIOS || /Android|BlackBerry|Opera Mini|IEMobile/i.test(navigator.userAgent)

    this.addEventListener('leave', this.pauseVideo.bind(this))
    this.addEventListener('enter-fade-start', this.resumeVideoProgress.bind(this))
    this.addEventListener('video-toggle', this.toggleVideo.bind(this))
    this.addEventListener('video-time', this.setVideoCurrentTime.bind(this))
  }

  PANOLENS.VideoPanorama.prototype = Object.create(PANOLENS.Panorama.prototype)

  PANOLENS.VideoPanorama.constructor = PANOLENS.VideoPanorama

  /**
	 * Load video panorama
	 * @param  {string} src     - The video url
	 * @param  {object} options - Option object containing videoElement
	 * @fires  PANOLENS.Panorama#panolens-viewer-handler
	 */
  PANOLENS.VideoPanorama.prototype.load = function (src, options) {
    var scope = this

    src = (src || this.src) || ''
    options = (options || this.options) || {}

    this.videoElement = options.videoElement || document.createElement('video')

    this.videoElement.muted = options.muted || false
    this.videoElement.loop = (options.loop !== undefined) ? options.loop : true
    this.videoElement.autoplay = (options.autoplay !== undefined) ? options.autoplay : false
    this.videoElement.crossOrigin = (options.crossOrigin !== undefined) ? options.crossOrigin : 'anonymous'

    // iphone inline player
    if (options.playsinline) {
      this.videoElement.setAttribute('playsinline', '')
      this.videoElement.setAttribute('webkit-playsinline', '')
    }

    var onloadeddata = function () {
      scope.onProgress({ loaded: 1, total: 1 })
      scope.setVideoTexture(scope.videoElement)

      if (scope.videoElement.autoplay) {
        /**
				 * Viewer handler event
				 * @type {object}
				 * @property {string} method - 'updateVideoPlayButton'
				 * @property {boolean} data - Pause video or not
				 */
        scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: false })
      }

      // For mobile silent autoplay
      if (scope.isMobile) {
        if (scope.videoElement.autoplay && scope.videoElement.muted) {
          /**
					 * Viewer handler event
					 * @type {object}
					 * @property {string} method - 'updateVideoPlayButton'
					 * @property {boolean} data - Pause video or not
					 */
          scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: false })
        } else {
          /**
					 * Viewer handler event
					 * @type {object}
					 * @property {string} method - 'updateVideoPlayButton'
					 * @property {boolean} data - Pause video or not
					 */
          scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: true })
        }
      }

      scope.onLoad()
    }

    /**
		 * Ready state of the audio/video element
		 * 0 = HAVE_NOTHING - no information whether or not the audio/video is ready
		 * 1 = HAVE_METADATA - metadata for the audio/video is ready
		 * 2 = HAVE_CURRENT_DATA - data for the current playback position is available, but not enough data to play next frame/millisecond
		 * 3 = HAVE_FUTURE_DATA - data for the current and at least the next frame is available
		 * 4 = HAVE_ENOUGH_DATA - enough data available to start playing
		 */
    if (this.videoElement.readyState > 2) {
      onloadeddata()
    } else {
      if (!this.videoElement.querySelectorAll('source').length || !this.videoElement.src) {
        this.videoElement.src = src
      }

      this.videoElement.load()
    }

    this.videoElement.onloadeddata = onloadeddata

    this.videoElement.ontimeupdate = function (event) {
      scope.videoProgress = this.duration >= 0 ? this.currentTime / this.duration : 0

      /**
			 * Viewer handler event
			 * @type {object}
			 * @property {string} method - 'onVideoUpdate'
			 * @property {number} data - The percentage of video progress. Range from 0.0 to 1.0
			 */
      scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'onVideoUpdate', data: scope.videoProgress })
    }

    this.videoElement.addEventListener('ended', function () {
      if (!scope.options.loop) {
        scope.resetVideo()
        scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: true })
      }
    }, false)
  }

  /**
	 * Set video texture
	 * @param {HTMLVideoElement} video  - The html5 video element
	 * @fires PANOLENS.Panorama#panolens-viewer-handler
	 */
  PANOLENS.VideoPanorama.prototype.setVideoTexture = function (video) {
    var videoTexture, videoRenderObject, scene

    if (!video) return

    videoTexture = new THREE.VideoTexture(video)
    videoTexture.minFilter = THREE.LinearFilter
    videoTexture.magFilter = THREE.LinearFilter
    videoTexture.format = THREE.RGBFormat

    videoRenderObject = {

      video: video,
      videoTexture: videoTexture

    }

    if (this.isIOS) {
      enableInlineVideo(video)
    }

    this.updateTexture(videoTexture)

    this.videoRenderObject = videoRenderObject
  }

  PANOLENS.VideoPanorama.prototype.reset = function () {
    this.videoElement = undefined

    PANOLENS.Panorama.prototype.reset.call(this)
  }

  /**
	 * Check if video is paused
	 * @return {boolean} - is video paused or not
	 */
  PANOLENS.VideoPanorama.prototype.isVideoPaused = function () {
    return this.videoRenderObject.video.paused
  }

  /**
	 * Toggle video to play or pause
	 */
  PANOLENS.VideoPanorama.prototype.toggleVideo = function () {
    if (this.videoRenderObject && this.videoRenderObject.video) {
      if (this.isVideoPaused()) {
        this.videoRenderObject.video.play()
      } else {
        this.videoRenderObject.video.pause()
      }
    }
  }

  /**
	 * Set video currentTime
	 * @param {object} event - Event contains percentage. Range from 0.0 to 1.0
	 */
  PANOLENS.VideoPanorama.prototype.setVideoCurrentTime = function (event) {
    if (this.videoRenderObject && this.videoRenderObject.video && !Number.isNaN(event.percentage) && event.percentage !== 1) {
      this.videoRenderObject.video.currentTime = this.videoRenderObject.video.duration * event.percentage

      this.dispatchEvent({ type: 'panolens-viewer-handler', method: 'onVideoUpdate', data: event.percentage })
    }
  }

  /**
	 * Play video
	 */
  PANOLENS.VideoPanorama.prototype.playVideo = function () {
    if (this.videoRenderObject && this.videoRenderObject.video && this.isVideoPaused()) {
      this.videoRenderObject.video.play()
    }

    /**
		 * Play event
		 * @type {object}
		 * @event 'play'
		 * */
    this.dispatchEvent({ type: 'play' })
  }

  /**
	 * Pause video
	 */
  PANOLENS.VideoPanorama.prototype.pauseVideo = function () {
    if (this.videoRenderObject && this.videoRenderObject.video && !this.isVideoPaused()) {
      this.videoRenderObject.video.pause()
    }

    /**
		 * Pause event
		 * @type {object}
		 * @event 'pause'
		 * */
    this.dispatchEvent({ type: 'pause' })
  }

  /**
	 * Resume video
	 */
  PANOLENS.VideoPanorama.prototype.resumeVideoProgress = function () {
    if (this.videoElement.autoplay && !this.isMobile) {
      this.playVideo()

      /**
			 * Viewer handler event
			 * @type {object}
			 * @property {string} method - 'updateVideoPlayButton'
			 * @property {boolean} data - Pause video or not
			 */
      this.dispatchEvent({ type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: false })
    } else {
      this.pauseVideo()

      /**
			 * Viewer handler event
			 * @type {object}
			 * @property {string} method - 'updateVideoPlayButton'
			 * @property {boolean} data - Pause video or not
			 */
      this.dispatchEvent({ type: 'panolens-viewer-handler', method: 'updateVideoPlayButton', data: true })
    }

    this.setVideoCurrentTime({ percentage: this.videoProgress })
  }

  /**
	 * Reset video at stating point
	 */
  PANOLENS.VideoPanorama.prototype.resetVideo = function () {
    if (this.videoRenderObject && this.videoRenderObject.video) {
      this.setVideoCurrentTime({ percentage: 0 })
    }
  }

  /**
	* Check if video is muted
	* @return {boolean} - is video muted or not
	*/
  PANOLENS.VideoPanorama.prototype.isVideoMuted = function () {
    return this.videoRenderObject.video.muted
  }

  /**
	 * Mute video
	 */
  PANOLENS.VideoPanorama.prototype.muteVideo = function () {
    if (this.videoRenderObject && this.videoRenderObject.video && !this.isVideoMuted()) {
      this.videoRenderObject.video.muted = true
    }

    this.dispatchEvent({ type: 'volumechange' })
  }

  /**
	 * Unmute video
	 */
  PANOLENS.VideoPanorama.prototype.unmuteVideo = function () {
    if (this.videoRenderObject && this.videoRenderObject.video && this.isVideoMuted()) {
      this.videoRenderObject.video.muted = false
    }

    this.dispatchEvent({ type: 'volumechange' })
  }

  /**
	 * Returns the video element
	 */
  PANOLENS.VideoPanorama.prototype.getVideoElement = function () {
    return this.videoRenderObject.video
  }

  /**
	 * Dispose video panorama
	 */
  PANOLENS.VideoPanorama.prototype.dispose = function () {
    this.resetVideo()
    this.pauseVideo()

    this.removeEventListener('leave', this.pauseVideo.bind(this))
    this.removeEventListener('enter-fade-start', this.resumeVideoProgress.bind(this))
    this.removeEventListener('video-toggle', this.toggleVideo.bind(this))
    this.removeEventListener('video-time', this.setVideoCurrentTime.bind(this))

    PANOLENS.Panorama.prototype.dispose.call(this)
  }
})(); (function () {
  'use strict'

  /**
	 * Empty panorama
	 * @constructor
	 * @param {number} [radius=5000] - Radius of panorama
	 */
  PANOLENS.EmptyPanorama = function (radius) {
    radius = radius || 5000

    var geometry = new THREE.Geometry(),
      material = new THREE.MeshBasicMaterial({
        color: 0x000000, opacity: 1, transparent: true
      })

    PANOLENS.Panorama.call(this, geometry, material)
  }

  PANOLENS.EmptyPanorama.prototype = Object.create(PANOLENS.Panorama.prototype)

  PANOLENS.EmptyPanorama.prototype.constructor = PANOLENS.EmptyPanorama
})(); (function () {
  /**
	 * Little Planet
	 * @constructor
	 * @param {string} type 		- Type of little planet basic class
	 * @param {string} source 		- URL for the image source
	 * @param {number} [size=10000] - Size of plane geometry
	 * @param {number} [ratio=0.5]  - Ratio of plane geometry's height against width
	 */
  PANOLENS.LittlePlanet = function (type, source, size, ratio) {
    type = type || 'image'

    type === 'image' && PANOLENS.ImagePanorama.call(this, source, size)

    this.size = size || 10000
    this.ratio = ratio || 0.5
    this.EPS = 0.000001
    this.frameId

    this.geometry = this.createGeometry()
    this.material = this.createMaterial(this.size)

    this.dragging = false
    this.userMouse = new THREE.Vector2()

    this.quatA = new THREE.Quaternion()
    this.quatB = new THREE.Quaternion()
    this.quatCur = new THREE.Quaternion()
    this.quatSlerp = new THREE.Quaternion()

    this.vectorX = new THREE.Vector3(1, 0, 0)
    this.vectorY = new THREE.Vector3(0, 1, 0)

    this.addEventListener('window-resize', this.onWindowResize)
  }

  PANOLENS.LittlePlanet.prototype = Object.create(PANOLENS.ImagePanorama.prototype)

  PANOLENS.LittlePlanet.prototype.constructor = PANOLENS.LittlePlanet

  PANOLENS.LittlePlanet.prototype.createGeometry = function () {
    return new THREE.PlaneGeometry(this.size, this.size * this.ratio)
  }

  PANOLENS.LittlePlanet.prototype.createMaterial = function (size) {
    var uniforms = PANOLENS.StereographicShader.uniforms
    uniforms.zoom.value = size

    return new THREE.ShaderMaterial({

      uniforms: uniforms,
      vertexShader: PANOLENS.StereographicShader.vertexShader,
      fragmentShader: PANOLENS.StereographicShader.fragmentShader

    })
  }

  PANOLENS.LittlePlanet.prototype.registerMouseEvents = function () {
    this.container.addEventListener('mousedown', this.onMouseDown.bind(this), false)
    this.container.addEventListener('mousemove', this.onMouseMove.bind(this), false)
    this.container.addEventListener('mouseup', this.onMouseUp.bind(this), false)
    this.container.addEventListener('touchstart', this.onMouseDown.bind(this), false)
    this.container.addEventListener('touchmove', this.onMouseMove.bind(this), false)
    this.container.addEventListener('touchend', this.onMouseUp.bind(this), false)
    this.container.addEventListener('mousewheel', this.onMouseWheel.bind(this), false)
    this.container.addEventListener('DOMMouseScroll', this.onMouseWheel.bind(this), false)
    this.container.addEventListener('contextmenu', this.onContextMenu.bind(this), false)
  }

  PANOLENS.LittlePlanet.prototype.unregisterMouseEvents = function () {
    this.container.removeEventListener('mousedown', this.onMouseDown.bind(this), false)
    this.container.removeEventListener('mousemove', this.onMouseMove.bind(this), false)
    this.container.removeEventListener('mouseup', this.onMouseUp.bind(this), false)
    this.container.removeEventListener('touchstart', this.onMouseDown.bind(this), false)
    this.container.removeEventListener('touchmove', this.onMouseMove.bind(this), false)
    this.container.removeEventListener('touchend', this.onMouseUp.bind(this), false)
    this.container.removeEventListener('mousewheel', this.onMouseWheel.bind(this), false)
    this.container.removeEventListener('DOMMouseScroll', this.onMouseWheel.bind(this), false)
    this.container.removeEventListener('contextmenu', this.onContextMenu.bind(this), false)
  }

  PANOLENS.LittlePlanet.prototype.onMouseDown = function (event) {
    var x = (event.clientX >= 0) ? event.clientX : event.touches[ 0 ].clientX
    var y = (event.clientY >= 0) ? event.clientY : event.touches[ 0 ].clientY

    var inputCount = (event.touches && event.touches.length) || 1

    switch (inputCount) {
      case 1:

        this.dragging = true
        this.userMouse.set(x, y)

        break

      case 2:

        var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX
        var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY
        var distance = Math.sqrt(dx * dx + dy * dy)
        this.userMouse.pinchDistance = distance

        break

      default:

        break
    }

    this.onUpdateCallback()
  }

  PANOLENS.LittlePlanet.prototype.onMouseMove = function (event) {
    var x = (event.clientX >= 0) ? event.clientX : event.touches[ 0 ].clientX
    var y = (event.clientY >= 0) ? event.clientY : event.touches[ 0 ].clientY

    var inputCount = (event.touches && event.touches.length) || 1

    switch (inputCount) {
      case 1:

        var angleX = THREE.Math.degToRad(x - this.userMouse.x) * 0.4
        var angleY = THREE.Math.degToRad(y - this.userMouse.y) * 0.4

        if (this.dragging) {
          this.quatA.setFromAxisAngle(this.vectorY, angleX)
          this.quatB.setFromAxisAngle(this.vectorX, angleY)
          this.quatCur.multiply(this.quatA).multiply(this.quatB)
          this.userMouse.set(x, y)
        }

        break

      case 2:

        var uniforms = this.material.uniforms
        var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX
        var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY
        var distance = Math.sqrt(dx * dx + dy * dy)

        this.addZoomDelta(this.userMouse.pinchDistance - distance)

        break

      default:

        break
    }
  }

  PANOLENS.LittlePlanet.prototype.onMouseUp = function (event) {
    this.dragging = false
  }

  PANOLENS.LittlePlanet.prototype.onMouseWheel = function (event) {
    event.preventDefault()
    event.stopPropagation()

    var delta = 0

    if (event.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9
      delta = event.wheelDelta
    } else if (event.detail !== undefined) { // Firefox
      delta = -event.detail
    }

    this.addZoomDelta(delta)
    this.onUpdateCallback()
  }

  PANOLENS.LittlePlanet.prototype.addZoomDelta = function (delta) {
    var uniforms = this.material.uniforms
    var lowerBound = this.size * 0.1
    var upperBound = this.size * 10

    uniforms.zoom.value += delta

    if (uniforms.zoom.value <= lowerBound) {
      uniforms.zoom.value = lowerBound
    } else if (uniforms.zoom.value >= upperBound) {
      uniforms.zoom.value = upperBound
    }
  }

  PANOLENS.LittlePlanet.prototype.onUpdateCallback = function () {
    this.frameId = window.requestAnimationFrame(this.onUpdateCallback.bind(this))

    this.quatSlerp.slerp(this.quatCur, 0.1)
    this.material.uniforms.transform.value.makeRotationFromQuaternion(this.quatSlerp)

    if (!this.dragging && 1.0 - this.quatSlerp.clone().dot(this.quatCur) < this.EPS) {
      window.cancelAnimationFrame(this.frameId)
    }
  }

  PANOLENS.LittlePlanet.prototype.reset = function () {
    this.quatCur.set(0, 0, 0, 1)
    this.quatSlerp.set(0, 0, 0, 1)
    this.onUpdateCallback()
  }

  PANOLENS.LittlePlanet.prototype.onLoad = function () {
    this.material.uniforms.resolution.value = this.container.clientWidth / this.container.clientHeight

    this.registerMouseEvents()
    this.onUpdateCallback()

    this.dispatchEvent({ type: 'panolens-viewer-handler', method: 'disableControl' })
  }

  PANOLENS.LittlePlanet.prototype.onLeave = function () {
    this.unregisterMouseEvents()

    this.dispatchEvent({ type: 'panolens-viewer-handler', method: 'enableControl', data: PANOLENS.Controls.ORBIT })

    window.cancelAnimationFrame(this.frameId)

    PANOLENS.Panorama.prototype.onLeave.call(this)
  }

  PANOLENS.LittlePlanet.prototype.onWindowResize = function () {
    this.material.uniforms.resolution.value = this.container.clientWidth / this.container.clientHeight
  }

  PANOLENS.LittlePlanet.prototype.onContextMenu = function () {
    this.dragging = false
  }
})(); (function () {
  /**
	 * Image Little Planet
	 * @constructor
	 * @param {string} source 		- URL for the image source
	 * @param {number} [size=10000] - Size of plane geometry
	 * @param {number} [ratio=0.5]  - Ratio of plane geometry's height against width
	 */
  PANOLENS.ImageLittlePlanet = function (source, size, ratio) {
    PANOLENS.LittlePlanet.call(this, 'image', source, size, ratio)
  }

  PANOLENS.ImageLittlePlanet.prototype = Object.create(PANOLENS.LittlePlanet.prototype)

  PANOLENS.ImageLittlePlanet.prototype.constructor = PANOLENS.ImageLittlePlanet

  PANOLENS.ImageLittlePlanet.prototype.onLoad = function (texture) {
    this.updateTexture(texture)

    PANOLENS.ImagePanorama.prototype.onLoad.call(this, texture)
    PANOLENS.LittlePlanet.prototype.onLoad.call(this)
  }

  PANOLENS.ImageLittlePlanet.prototype.updateTexture = function (texture) {
    texture.minFilter = texture.magFilter = THREE.LinearFilter

    this.material.uniforms[ 'tDiffuse' ].value = texture
  }
})(); (function () {
  /**
	 * Reticle 3D Sprite
	 * @constructor
	 * @param {THREE.Color} [color=0xfffff] - Color of the reticle sprite
	 * @param {boolean} [autoSelect=true] - Auto selection
	 * @param {string} [idleImageUrl=PANOLENS.DataImage.ReticleIdle] - Image asset url
	 * @param {string} [dwellImageUrl=PANOLENS.DataImage.ReticleDwell] - Image asset url
	 * @param {number} [dwellTime=1500] - Duration for dwelling sequence to complete
	 * @param {number} [dwellSpriteAmount=45] - Number of dwelling sprite sequence
	 */
  PANOLENS.Reticle = function (color, autoSelect, idleImageUrl, dwellImageUrl, dwellTime, dwellSpriteAmount) {
    color = color || 0xffffff

    this.autoSelect = autoSelect != undefined ? autoSelect : true

    this.dwellTime = dwellTime || 1500
    this.dwellSpriteAmount = dwellSpriteAmount || 45
    this.dwellInterval = this.dwellTime / this.dwellSpriteAmount

    this.IDLE = 0
    this.DWELLING = 1
    this.status

    this.scaleIdle = new THREE.Vector3(0.2, 0.2, 1)
    this.scaleDwell = new THREE.Vector3(1, 0.8, 1)

    this.textureLoaded = false
    this.idleImageUrl = idleImageUrl || PANOLENS.DataImage.ReticleIdle
    this.dwellImageUrl = dwellImageUrl || PANOLENS.DataImage.ReticleDwell
    this.idleTexture = new THREE.Texture()
    this.dwellTexture = new THREE.Texture()

    THREE.Sprite.call(this, new THREE.SpriteMaterial({ color: color, depthTest: false }))

    this.currentTile = 0
    this.startTime = 0

    this.visible = false
    this.renderOrder = 10
    this.timerId

    // initial update
    this.updateStatus(this.IDLE)
  }

  PANOLENS.Reticle.prototype = Object.create(THREE.Sprite.prototype)

  PANOLENS.Reticle.prototype.constructor = PANOLENS.Reticle

  /**
	 * Make reticle visible
	 */
  PANOLENS.Reticle.prototype.show = function () {
    this.visible = true
  }

  /**
	 * Make reticle invisible
	 */
  PANOLENS.Reticle.prototype.hide = function () {
    this.visible = false
  }

  /**
	 * Load reticle textures
	 */
  PANOLENS.Reticle.prototype.loadTextures = function () {
    this.idleTexture = PANOLENS.Utils.TextureLoader.load(this.idleImageUrl)
    this.dwellTexture = PANOLENS.Utils.TextureLoader.load(this.dwellImageUrl)

    this.material.map = this.idleTexture
    this.setupDwellSprite(this.dwellTexture)
    this.textureLoaded = true
  }

  /**
	 * Start reticle timer selection
	 * @param  {function} completeCallback - Callback after dwell completes
	 */
  PANOLENS.Reticle.prototype.select = function (completeCallback) {
    if (performance.now() - this.startTime >= this.dwellTime) {
      this.completeDwelling()
      completeCallback()
    } else if (this.autoSelect) {
      this.updateDwelling(performance.now())
      this.timerId = window.requestAnimationFrame(this.select.bind(this, completeCallback))
    }
  }

  /**
	 * Clear and reset reticle timer
	 */
  PANOLENS.Reticle.prototype.clearTimer = function () {
    window.cancelAnimationFrame(this.timerId)
    this.timerId = null
  }

  /**
	 * Setup dwell sprite animation
	 */
  PANOLENS.Reticle.prototype.setupDwellSprite = function (texture) {
    texture.wrapS = THREE.RepeatWrapping
    texture.repeat.set(1 / this.dwellSpriteAmount, 1)
  }

  /**
	 * Update reticle status
	 * @param {number} status - Reticle status
	 */
  PANOLENS.Reticle.prototype.updateStatus = function (status) {
    this.status = status

    if (status === this.IDLE) {
      this.scale.copy(this.scaleIdle)
      this.material.map = this.idleTexture
    } else if (status === this.DWELLING) {
      this.scale.copy(this.scaleDwell)
      this.material.map = this.dwellTexture
    }

    this.currentTile = 0
    this.material.map.offset.x = 0
  }

  /**
	 * Start dwelling sequence
	 */
  PANOLENS.Reticle.prototype.startDwelling = function (completeCallback) {
    if (!this.autoSelect) {
      return
    }

    this.startTime = performance.now()
    this.updateStatus(this.DWELLING)
    this.select(completeCallback)
  }

  /**
	 * Update dwelling sequence
	 * @param  {number} time - Timestamp for elasped time
	 */
  PANOLENS.Reticle.prototype.updateDwelling = function (time) {
    var elasped = time - this.startTime

    if (this.currentTile <= this.dwellSpriteAmount) {
      this.currentTile = Math.floor(elasped / this.dwellTime * this.dwellSpriteAmount)
      this.material.map.offset.x = this.currentTile / this.dwellSpriteAmount
    } else {
      this.updateStatus(this.IDLE)
    }
  }

  /**
	 * Cancel dwelling
	 */
  PANOLENS.Reticle.prototype.cancelDwelling = function () {
    this.clearTimer()
    this.updateStatus(this.IDLE)
  }

  /**
	 * Complete dwelling
	 */
  PANOLENS.Reticle.prototype.completeDwelling = function () {
    this.clearTimer()
    this.updateStatus(this.IDLE)
  }
})(); (function () {
  /**
	 * Creates a tile with bent capability
	 * @constructor
	 * @param {number}  [width=10]                      				- Width along the X axis
	 * @param {number}  [height=5]                      				- Height along the Y axis
	 * @param {number}  [widthSegments=20]              				- Width segments
	 * @param {number}  [heightSegments=20]             				- Height segments
	 * @param {THREE.Vector3} [forceDirection=THREE.Vector3( 0, 0, 1 )] - Force direction
	 * @param {THREE.Vector3} [forceAxis=THREE.Vector3( 0, 1, 0 )] 		- Along this axis
	 * @param {number} [forceAngle=Math.PI/12] 							- Angle to bend in radians
	 */
  PANOLENS.Tile = function (width, height, widthSegments, heightSegments, forceDirection, forceAxis, forceAngle) {
    var scope = this

    this.parameters = {
      width: width,
      height: height,
      widthSegments: widthSegments,
      heightSegments: heightSegments,
      forceDirection: forceDirection,
      forceAxis: forceAxis,
      forceAngle: forceAngle
    }

    width = width || 10
    height = height || 5
    widthSegments = widthSegments || 1
    heightSegments = heightSegments || 1
    forceDirection = forceDirection || new THREE.Vector3(0, 0, 1)
    forceAxis = forceAxis || new THREE.Vector3(0, 1, 0)
    forceAngle = forceAngle !== undefined ? forceAngle : 0

    THREE.Mesh.call(this,
      new THREE.PlaneGeometry(width, height, widthSegments, heightSegments),
      new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true })
    )

    this.bendModifier = new THREE.BendModifier()

    this.entity = undefined

    this.animationDuration = 500
    this.animationFadeOut = undefined
    this.animationFadeIn = undefined
    this.animationTranslation = undefined
    this.tweens = {}

    if (forceAngle !== 0) {
      this.bend(forceDirection, forceAxis, forceAngle)
    }

    this.originalGeometry = this.geometry.clone()
  }

  PANOLENS.Tile.prototype = Object.create(THREE.Mesh.prototype)

  PANOLENS.Tile.prototype.constructor = PANOLENS.Tile

  PANOLENS.Tile.prototype.clone = function () {
    var parameters = this.parameters, tile

    tile = new PANOLENS.Tile(
      parameters.width,
      parameters.height,
      parameters.widthSegments,
      parameters.heightSegments,
      parameters.forceDirection,
      parameters.forceAxis,
      parameters.forceAngle
    )

    tile.setEntity(this.entity)
    tile.material = this.material.clone()

    return tile
  }

  /**
	 * Bend panel with direction, axis, and angle
	 * @param  {THREE.Vector3} direction - Force direction
	 * @param  {THREE.Vector3} axis - Along this axis
	 * @param  {number} angle - Angle to bend in radians
	 */
  PANOLENS.Tile.prototype.bend = function (direction, axis, angle) {
    this.bendModifier.set(direction, axis, angle).modify(this.geometry)
  }

  /**
	 * Restore geometry back to initial state
	 */
  PANOLENS.Tile.prototype.unbend = function () {
    var geometry = this.geometry

    this.geometry = this.originalGeometry
    this.originalGeometry = this.geometry.clone()

    geometry.dispose()
    geometry = null
  }

  /**
	 * Create a tween object for animation
	 * based on - {@link https://github.com/tweenjs/tween.js}
	 * @param  {string} name       - Name of the tween animation
	 * @param  {object} object     - Object to be tweened
	 * @param  {object} toState    - Final state of the object's properties
	 * @param  {number} duration   - Tweening duration
	 * @param  {TWEEN.Easing} easing     - Easing function
	 * @param  {number} delay      - Animation delay time
	 * @param  {Function} onStart    - On start function
	 * @param  {Function} onUpdate   - On update function
	 * @param  {Function} onComplete - On complete function
	 * @return {TWEEN.Tween}         - Tween object
	 */
  PANOLENS.Tile.prototype.tween = function (name, object, toState, duration, easing, delay, onStart, onUpdate, onComplete) {
    object = object || this
    	toState = toState || {}
    	duration = duration || this.animationDuration
    	easing = easing || TWEEN.Easing.Exponential.Out
    	delay = delay !== undefined ? delay : 0
    	onStart = onStart || null
    	onUpdate = onUpdate || null
    	onComplete = onComplete || null

    	if (!this.tweens[name]) {
    		this.tweens[name] = new TWEEN.Tween(object)
    			.to(toState, duration)
	        	.easing(easing)
	        	.delay(delay)
	        	.onStart(onStart)
	        	.onUpdate(onUpdate)
	        	.onComplete(onComplete)
    	}

    	return this.tweens[name]
  }

  /**
     * Short-hand for displaying a single ripple effect
     * by duplicating itself and fadeout
     * @param  {number} scale    - The duplicated self fadeout scale
     * @param  {number} duration - Effect duration
     * @param  {TWEEN.Easing} easing   - Easing function
     */
  PANOLENS.Tile.prototype.ripple = function (scale, duration, easing) {
    	scale = scale || 2
    	duration = duration || 200
    	easing = easing || TWEEN.Easing.Cubic.Out

    	var scope = this, ripple = this.clone()

    new TWEEN.Tween(ripple.scale)
      .to({x: scale, y: scale}, duration)
      .easing(easing)
      .start()

    new TWEEN.Tween(ripple.material)
      .to({opacity: 0}, duration)
      .easing(easing)
      .onComplete(function () {
        scope.remove(ripple)
        ripple.geometry.dispose()
        ripple.material.dispose()
      })
      .start()

    this.add(ripple)
  }

  /**
	 * Set entity if multiple objects are considered as one entity
	 * @param {object} entity - Entity represents whole group structure
	 */
  PANOLENS.Tile.prototype.setEntity = function (entity) {
    this.entity = entity
  }
})(); (function () {
  'use strict'

  /**
     * Group consists of tile array
     * @constructor
     * @param {array}  tileArray         - Tile array of PANOLENS.Tile
     * @param {number} verticalGap       - Vertical gap between each tile
     * @param {number} depthGap          - Depth gap between each tile
     * @param {number} animationDuration - Animation duration
     * @param {number} offset            - Offset index
     */
  PANOLENS.TileGroup = function (tileArray, verticalGap, depthGap, animationDuration, offset) {
    var scope = this, textureLoader

    THREE.Object3D.call(this)

    this.tileArray = tileArray || []
    this.offset = offset !== undefined ? offset : 0
    this.v_gap = verticalGap !== undefined ? verticalGap : 6
    this.d_gap = depthGap !== undefined ? depthGap : 2
    this.animationDuration = animationDuration !== undefined ? animationDuration : 200
    this.animationEasing = TWEEN.Easing.Exponential.Out
    this.visibleDelta = 2

    this.tileArray.map(function (tile, index) {
      if (tile.image) {
        PANOLENS.Utils.TextureLoader.load(tile.image, scope.setTexture.bind(tile))
      }

      tile.position.set(0, index * -scope.v_gap, index * -scope.d_gap)
      tile.originalPosition = tile.position.clone()
      tile.setEntity(scope)
      scope.add(tile)
    })

    this.updateVisbility()
  }

  PANOLENS.TileGroup.prototype = Object.create(THREE.Object3D.prototype)

  PANOLENS.TileGroup.prototype.constructor = PANOLENS.TileGroup

  /**
     * Update corresponding tile textures
     * @param  {array} imageArray - Image array with index to index image update
     */
  PANOLENS.TileGroup.prototype.updateTexture = function (imageArray) {
    var scope = this

    imageArray = imageArray || []
    this.children.map(function (child, index) {
      if (child instanceof PANOLENS.Tile && imageArray[index]) {
        PANOLENS.Utils.TextureLoader.load(imageArray[index], scope.setTexture.bind(child))
	    		if (child.outline) {
	    			child.outline.material.visible = true
	    		}
      }
    })
  }

  /**
     * Update all tile textures and hide the remaining ones
     * @param  {array} imageArray - Image array with index to index image update
     */
  PANOLENS.TileGroup.prototype.updateAllTexture = function (imageArray) {
    this.updateTexture(imageArray)

    if (imageArray.length < this.children.length) {
      for (var i = imageArray.length; i < this.children.length; i++) {
        if (this.children[i] instanceof PANOLENS.Tile) {
          this.children[i].material.visible = false
          if (this.children[i].outline) {
            this.children[i].outline.material.visible = false
          }
        }
      }
    }
  }

  /**
     * Set individual texture
     * @param {THREE.Texture} texture - Texture to be updated
     */
  PANOLENS.TileGroup.prototype.setTexture = function (texture) {
    texture.minFilter = THREE.LinearFilter
    texture.magFilter = THREE.LinearFilter
    this.material.visible = true
    this.material.map = texture
    this.material.needsUpdate = true
  }

  /**
     * Update visibility
     */
  PANOLENS.TileGroup.prototype.updateVisbility = function () {
    	this.children[this.offset].visible = true
    	new TWEEN.Tween(this.children[this.offset].material)
      .to({ opacity: 1 }, this.animationDuration)
      .easing(this.animationEasing)
      .start()

    	if (this.children[this.offset].outline) {
    		this.children[this.offset].outline.visible = true
    	}

    	// Backward
    	for (var i = this.offset - 1; i >= 0; i--) {
    		if (this.tileArray.indexOf(this.children[i]) === -1) { continue }

    		if (this.offset - i <= this.visibleDelta) {
    			this.children[i].visible = true
    			new TWEEN.Tween(this.children[i].material)
    			.to({ opacity: 1 / (this.offset - i) * 0.5 }, this.animationDuration)
    			.easing(this.animationEasing)
    			.start()
    		} else {
    			this.children[i].visible = false
    		}

    		this.children[i].outline && (this.children[i].outline.visible = false)
    	}

    	// Forward
    	for (var i = this.offset + 1; i < this.children.length; i++) {
    		if (this.tileArray.indexOf(this.children[i]) === -1) { continue }

    		if (i - this.offset <= this.visibleDelta) {
    			this.children[i].visible = true
    			new TWEEN.Tween(this.children[i].material)
    			.to({ opacity: 1 / (i - this.offset) * 0.5 }, this.animationDuration)
    			.easing(this.animationEasing)
    			.start()
    		} else {
    			this.children[i].visible = false
    		}

    		this.children[i].outline && (this.children[i].outline.visible = false)
    	}
  }

  /**
     * Scroll up
     * @param  {number} duration - Scroll up duration
     */
  PANOLENS.TileGroup.prototype.scrollUp = function (duration) {
    	var tiles = this.tileArray, offset

    	duration = duration !== undefined ? duration : this.animationDuration

    	offset = this.offset + 1

    	if (this.offset < tiles.length - 1 && tiles[ this.offset + 1 ].material.visible) {
    		for (var i = tiles.length - 1; i >= 0; i--) {
    			new TWEEN.Tween(tiles[i].position)
    			.to({ y: (i - offset) * -this.v_gap,
    				  z: Math.abs(i - offset) * -this.d_gap }, duration)
    			.easing(this.animationEasing)
    			.start()
	    	}

	    	this.offset++
	    	this.updateVisbility()
	    	this.dispatchEvent({ type: 'scroll', direction: 'up' })
    	}
  }

  /**
     * Scroll down
     * @param  {number} duration - Scroll up duration
     */
  PANOLENS.TileGroup.prototype.scrollDown = function (duration) {
    	var tiles = this.tileArray, offset

    	duration = duration !== undefined ? duration : this.animationDuration

    	offset = this.offset - 1

    	if (this.offset > 0 && tiles[ this.offset - 1 ].material.visible) {
    		for (var i = 0; i < tiles.length; i++) {
	    		new TWEEN.Tween(tiles[i].position)
    			.to({ y: (i - offset) * -this.v_gap,
    				  z: Math.abs(i - offset) * -this.d_gap }, duration)
    			.easing(this.animationEasing)
    			.start()
	    	}

	    	this.offset--
	    	this.updateVisbility()
	    	this.dispatchEvent({ type: 'scroll', direction: 'down' })
    	}
  }

  PANOLENS.TileGroup.prototype.reset = function () {
    	this.tileArray.map(function (child, index) {
    		child.position.copy(child.originalPosition)
    	})

    	this.offset = 0
    	this.updateVisbility()
  }

  /**
     * Get current index
     * @return {number} Index of the group. Range from 0 to this.tileArray.length
     */
  PANOLENS.TileGroup.prototype.getIndex = function () {
    	return this.offset
  }

  /**
     * Get visible tile counts
     * @return {number} Number of visible tiles
     */
  PANOLENS.TileGroup.prototype.getTileCount = function () {
    	var count = 0

    	this.tileArray.map(function (tile) {
    		if (tile.material.visible) {
    			count++
    		}
    	})

    	return count
  }
})(); (function () {
  'use strict'

  var sharedFont, sharedTexture
  var pendingQueue = []

  /**
	 * Sprite text based on {@link https://github.com/Jam3/three-bmfont-text}
	 * @constructor
	 * @param {string} text     - Text to be displayed
	 * @param {number} maxWidth	- Max width
	 * @param {number} color    - Color in hexadecimal
	 * @param {number} opacity  - Text opacity
	 * @param {object} options  - Options to create text geometry
	 */
  PANOLENS.SpriteText = function (text, maxWidth, color, opacity, options) {
    THREE.Object3D.call(this)

    this.text = text || ''
    this.maxWidth = maxWidth || 2000
    this.color = color || 0xffffff
    this.opacity = opacity !== undefined ? opacity : 1
    this.options = options || {}

    this.animationDuration = 500
    this.animationFadeOut = undefined
    this.animationFadeIn = undefined
    this.tweens = {}

    this.addText(text)
  }

  PANOLENS.SpriteText.prototype = Object.create(THREE.Object3D.prototype)

  PANOLENS.SpriteText.prototype.constructor = PANOLENS.SpriteText

  // Reference function will be overwritten by Bmfont.js
  PANOLENS.SpriteText.prototype.generateTextGeometry = function () {}
  PANOLENS.SpriteText.prototype.generateSDFShader = function () {}

  /**
	 * Set BMFont
	 * @param {Function} callback - Callback after font is loaded
	 * @param {object}   font     - The font to be loaded
	 * @param {THREE.Texture}   texture  - Font texture
	 */
  PANOLENS.SpriteText.prototype.setBMFont = function (callback, font, texture) {
    texture.needsUpdate = true
	  	texture.minFilter = THREE.LinearMipMapLinearFilter
    texture.magFilter = THREE.LinearFilter
    texture.generateMipmaps = true
    texture.anisotropy = 8

    sharedFont = font
    sharedTexture = texture

    for (var i = pendingQueue.length - 1; i >= 0; i--) {
      pendingQueue[ i ].target.addText(pendingQueue[ i ].text)
    }

    while (pendingQueue.length > 0) {
      pendingQueue.pop()
    }

    callback && callback()
  }

  /**
	 * Add text mesh
	 * @param {string} text - Text to be displayed
	 */
  PANOLENS.SpriteText.prototype.addText = function (text) {
    if (!sharedFont || !sharedTexture) {
      pendingQueue.push({ target: this, text: text })
      return
    }

    var textAnchor = new THREE.Object3D()

    this.options.text = text
    this.options.font = sharedFont
    this.options.width = this.maxWidth

    var geometry = this.generateTextGeometry(this.options)
    geometry.computeBoundingBox()
    geometry.computeBoundingSphere()

    var material = new THREE.RawShaderMaterial(this.generateSDFShader({
		    map: sharedTexture,
		    side: THREE.DoubleSide,
		    transparent: true,
		    color: this.color,
		    opacity: this.opacity
    }))

    var layout = geometry.layout
    var textMesh = new THREE.Mesh(geometry, material)

    textMesh.entity = this
    textMesh.position.x = -layout.width / 2
    textMesh.position.y = layout.height * 1.035

    textAnchor.scale.x = textAnchor.scale.y = -0.05
    textAnchor.add(textMesh)

    this.mesh = textMesh
    this.add(textAnchor)
  }

  /**
	 * Update text geometry
	 * @param  {object} options - Geometry options based on
	 *  https://github.com/Jam3/three-bmfont-text#geometry--createtextopt
	 */
  PANOLENS.SpriteText.prototype.update = function (options) {
    var mesh

    options = options || {}

    mesh = this.mesh

    mesh.geometry.update(options)
    mesh.position.x = -mesh.geometry.layout.width / 2
    mesh.position.y = mesh.geometry.layout.height * 1.035
  }

  /**
	 * Create a tween object for animation
	 * based on - {@link https://github.com/tweenjs/tween.js}
	 * @param  {string} name       - Name of the tween animation
	 * @param  {object} object     - Object to be tweened
	 * @param  {object} toState    - Final state of the object's properties
	 * @param  {number} duration   - Tweening duration
	 * @param  {TWEEN.Easing} easing     - Easing function
	 * @param  {number} delay      - Animation delay time
	 * @param  {Function} onStart    - On start function
	 * @param  {Function} onUpdate   - On update function
	 * @param  {Function} onComplete - On complete function
	 * @return {TWEEN.Tween}         - Tween object
	 */
  PANOLENS.SpriteText.prototype.tween = function (name, object, toState, duration, easing, delay, onStart, onUpdate, onComplete) {
    object = object || this
    toState = toState || {}
    duration = duration || this.animationDuration
    easing = easing || TWEEN.Easing.Exponential.Out
    delay = delay !== undefined ? delay : 0
    onStart = onStart || null
    onUpdate = onUpdate || null
    onComplete = onComplete || null

    if (!this.tweens[name]) {
      this.tweens[name] = new TWEEN.Tween(object)
        .to(toState, duration)
	        	.easing(easing)
	        	.delay(delay)
	        	.onStart(onStart)
	        	.onUpdate(onUpdate)
	        	.onComplete(onComplete)
    }

    return this.tweens[name]
  }

  /**
	 * Get geometry layout
	 * @return {object} Text geometry layout
	 */
  PANOLENS.SpriteText.prototype.getLayout = function () {
    return this.mesh && this.mesh.geometry && this.mesh.geometry.layout || {}
  }

  /**
	 * Set entity if multiple objects are considered as one entity
	 * @param {object} entity - Entity represents whole group structure
	 */
  PANOLENS.SpriteText.prototype.setEntity = function (entity) {
    this.entity = entity
  }
})(); (function () {
  /**
	 * Widget for controls
	 * @constructor
	 * @param {HTMLElement} container - A domElement where default control widget will be attached to
	 */
  PANOLENS.Widget = function (container) {
    THREE.EventDispatcher.call(this)

    this.DEFAULT_TRANSITION = 'all 0.27s ease'
    this.TOUCH_ENABLED = PANOLENS.Utils.checkTouchSupported()
    this.PREVENT_EVENT_HANDLER = function (event) {
      event.preventDefault()
      event.stopPropagation()
    }

    this.container = container

    this.barElement
    this.fullscreenElement
    this.videoElement
    this.settingElement

    this.mainMenu

    this.activeMainItem
    this.activeSubMenu
    this.mask
  }

  PANOLENS.Widget.prototype = Object.create(THREE.EventDispatcher.prototype)

  PANOLENS.Widget.prototype.constructor = PANOLENS.Widget

  /**
	 * Add control bar
	 */
  PANOLENS.Widget.prototype.addControlBar = function () {
    if (!this.container) {
      console.warn('Widget container not set')
      return
    }

    var scope = this, bar, styleTranslate, styleOpacity, gradientStyle

    gradientStyle = 'linear-gradient(bottom, rgba(0,0,0,0.2), rgba(0,0,0,0))'

    bar = document.createElement('div')
    bar.style.width = '100%'
    bar.style.height = '44px'
    bar.style.float = 'left'
    bar.style.transform = bar.style.webkitTransform = bar.style.msTransform = 'translateY(-100%)'
    bar.style.background = '-webkit-' + gradientStyle
    bar.style.background = '-moz-' + gradientStyle
    bar.style.background = '-o-' + gradientStyle
    bar.style.background = '-ms-' + gradientStyle
    bar.style.background = gradientStyle
    bar.style.transition = this.DEFAULT_TRANSITION
    bar.style.pointerEvents = 'none'
    bar.isHidden = false
    bar.toggle = function () {
      bar.isHidden = !bar.isHidden
      styleTranslate = bar.isHidden ? 'translateY(0)' : 'translateY(-100%)'
      styleOpacity = bar.isHidden ? 0 : 1
      bar.style.transform = bar.style.webkitTransform = bar.style.msTransform = styleTranslate
      bar.style.opacity = styleOpacity
    }

    // Menu
    var menu = this.createDefaultMenu()
    this.mainMenu = this.createMainMenu(menu)
    bar.appendChild(this.mainMenu)

    // Mask
    var mask = this.createMask()
    this.mask = mask
    this.container.appendChild(mask)

    // Dispose
    bar.dispose = function () {
      if (scope.fullscreenElement) {
        bar.removeChild(scope.fullscreenElement)
        scope.fullscreenElement.dispose()
        scope.fullscreenElement = null
      }

      if (scope.settingElement) {
        bar.removeChild(scope.settingElement)
        scope.settingElement.dispose()
        scope.settingElement = null
      }

      if (scope.videoElement) {
        bar.removeChild(scope.videoElement)
        scope.videoElement.dispose()
        scope.videoElement = null
      }
    }

    this.container.appendChild(bar)

    // Mask events
    this.mask.addEventListener('mousemove', this.PREVENT_EVENT_HANDLER, true)
    this.mask.addEventListener('mouseup', this.PREVENT_EVENT_HANDLER, true)
    this.mask.addEventListener('mousedown', this.PREVENT_EVENT_HANDLER, true)
    this.mask.addEventListener(scope.TOUCH_ENABLED ? 'touchend' : 'click', function (event) {
      event.preventDefault()
      event.stopPropagation()

      scope.mask.hide()
      scope.settingElement.deactivate()
    }, false)

    // Event listener
    this.addEventListener('control-bar-toggle', bar.toggle)

    this.barElement = bar
  }

  /**
	 * Create default menu
	 */
  PANOLENS.Widget.prototype.createDefaultMenu = function () {
    var scope = this, handler

    handler = function (method, data) {
      return function () {
        scope.dispatchEvent({

          type: 'panolens-viewer-handler',
          method: method,
          data: data

        })
      }
    }

    return [

      {
        title: 'Control',
        subMenu: [
          {
            title: this.TOUCH_ENABLED ? 'Touch' : 'Mouse',
            handler: handler('enableControl', PANOLENS.Controls.ORBIT)
          },
          {
            title: 'Sensor',
            handler: handler('enableControl', PANOLENS.Controls.DEVICEORIENTATION)
          }
        ]
      },

      {
        title: 'Mode',
        subMenu: [
          {
            title: 'Normal',
            handler: handler('disableEffect')
          },
          {
            title: 'Cardboard',
            handler: handler('enableEffect', PANOLENS.Modes.CARDBOARD)
          },
          {
            title: 'Stereoscopic',
            handler: handler('enableEffect', PANOLENS.Modes.STEREO)
          }
        ]
      }

    ]
  }

  /**
	 * Add buttons on top of control bar
	 * @param {string} name - The control button name to be created
	 */
  PANOLENS.Widget.prototype.addControlButton = function (name) {
    var element

    switch (name) {
      case 'fullscreen':

        element = this.createFullscreenButton()
        this.fullscreenElement = element

        break

      case 'setting':

        element = this.createSettingButton()
        this.settingElement = element

        break

      case 'video':

        element = this.createVideoControl()
        this.videoElement = element

        break

      default:

        return
    }

    if (!element) {
      return
    }

    this.barElement.appendChild(element)
  }

  PANOLENS.Widget.prototype.createMask = function () {
    var element = document.createElement('div')
    element.style.position = 'absolute'
    element.style.top = 0
    element.style.left = 0
    element.style.width = '100%'
    element.style.height = '100%'
    element.style.background = 'transparent'
    element.style.display = 'none'

    element.show = function () {
      this.style.display = 'block'
    }

    element.hide = function () {
      this.style.display = 'none'
    }

    return element
  }

  /**
	 * Create Setting button to toggle menu
	 */
  PANOLENS.Widget.prototype.createSettingButton = function () {
    var scope = this, item

    function onTap (event) {
      event.preventDefault()
      event.stopPropagation()

      scope.mainMenu.toggle()

      if (this.activated) {
        this.deactivate()
      } else {
        this.activate()
      }
    }

    item = this.createCustomItem({

      style: {

        backgroundImage: 'url("' + PANOLENS.DataImage.Setting + '")',
        webkitTransition: this.DEFAULT_TRANSITION,
        transition: this.DEFAULT_TRANSITION

      },

      onTap: onTap

    })

    item.activate = function () {
      this.style.transform = 'rotate3d(0,0,1,90deg)'
      this.activated = true
      scope.mask.show()
    }

    item.deactivate = function () {
      this.style.transform = 'rotate3d(0,0,0,0)'
      this.activated = false
      scope.mask.hide()

      if (scope.mainMenu && scope.mainMenu.visible) {
        scope.mainMenu.hide()
      }

      if (scope.activeSubMenu && scope.activeSubMenu.visible) {
        scope.activeSubMenu.hide()
      }

      if (scope.mainMenu && scope.mainMenu._width) {
        scope.mainMenu.changeSize(scope.mainMenu._width)
        scope.mainMenu.unslideAll()
      }
    }

    item.activated = false

    return item
  }

  /**
	 * Create Fullscreen button
	 * @return {HTMLSpanElement} - The dom element icon for fullscreen
	 * @fires PANOLENS.Widget#panolens-viewer-handler
	 */
  PANOLENS.Widget.prototype.createFullscreenButton = function () {
    var scope = this, item, isFullscreen = false, tapSkipped = true, stylesheetId

    stylesheetId = 'panolens-style-addon'

    // Don't create button if no support
    if (!document.fullscreenEnabled &&
			 !document.webkitFullscreenEnabled &&
			 !document.mozFullScreenEnabled &&
			 !document.msFullscreenEnabled) {
      return
    }

    function onTap (event) {
      event.preventDefault()
      event.stopPropagation()

      tapSkipped = false

      if (!isFullscreen) {
			    scope.container.requestFullscreen && scope.container.requestFullscreen()
			    scope.container.msRequestFullscreen && scope.container.msRequestFullscreen()
			    scope.container.mozRequestFullScreen && scope.container.mozRequestFullScreen()
			    scope.container.webkitRequestFullscreen && scope.container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)
        isFullscreen = true
      } else {
			    document.exitFullscreen && document.exitFullscreen()
			    document.msExitFullscreen && document.msExitFullscreen()
			    document.mozCancelFullScreen && document.mozCancelFullScreen()
			    document.webkitExitFullscreen && document.webkitExitFullscreen()
        isFullscreen = false
      }

      this.style.backgroundImage = (isFullscreen)
        ? 'url("' + PANOLENS.DataImage.FullscreenLeave + '")'
        : 'url("' + PANOLENS.DataImage.FullscreenEnter + '")'
    }

    function onFullScreenChange (e) {
      if (tapSkipped) {
        isFullscreen = !isFullscreen

        item.style.backgroundImage = (isFullscreen)
          ? 'url("' + PANOLENS.DataImage.FullscreenLeave + '")'
          : 'url("' + PANOLENS.DataImage.FullscreenEnter + '")'
      }

      /**
			 * Viewer handler event
			 * @type {object}
			 * @property {string} method - 'onWindowResize' function call on PANOLENS.Viewer
			 */
      scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'onWindowResize', data: false })

      tapSkipped = true
    }

    document.addEventListener('fullscreenchange', onFullScreenChange, false)
    document.addEventListener('webkitfullscreenchange', onFullScreenChange, false)
    document.addEventListener('mozfullscreenchange', onFullScreenChange, false)
    document.addEventListener('MSFullscreenChange', onFullScreenChange, false)

    item = this.createCustomItem({

      style: {

        backgroundImage: 'url("' + PANOLENS.DataImage.FullscreenEnter + '")'

      },

      onTap: onTap

    })

    // Add fullscreen stlye if not exists
    if (!document.querySelector(stylesheetId)) {
      var sheet = document.createElement('style')
      sheet.id = stylesheetId
      sheet.innerHTML = ':-webkit-full-screen { width: 100% !important; height: 100% !important }'
      document.body.appendChild(sheet)
    }

    return item
  }

  /**
	 * Create video control container
	 * @return {HTMLSpanElement} - The dom element icon for video control
	 */
  PANOLENS.Widget.prototype.createVideoControl = function () {
    var item

    item = document.createElement('span')
    item.style.display = 'none'
    item.show = function () {
      item.style.display = ''
    }

    item.hide = function () {
      item.style.display = 'none'
      item.controlButton.paused = true
      item.controlButton.update()
    }

    item.controlButton = this.createVideoControlButton()
    item.seekBar = this.createVideoControlSeekbar()

    item.appendChild(item.controlButton)
    item.appendChild(item.seekBar)

    item.dispose = function () {
      item.removeChild(item.controlButton)
      item.removeChild(item.seekBar)

      item.controlButton.dispose()
      item.controlButton = null

      item.seekBar.dispose()
      item.seekBar = null
    }

    this.addEventListener('video-control-show', item.show)
    this.addEventListener('video-control-hide', item.hide)

    return item
  }

  /**
	 * Create video control button
	 * @return {HTMLSpanElement} - The dom element icon for video control
	 * @fires PANOLENS.Widget#panolens-viewer-handler
	 */
  PANOLENS.Widget.prototype.createVideoControlButton = function () {
    var scope = this, item

    function onTap (event) {
      event.preventDefault()
      event.stopPropagation()

      /**
			 * Viewer handler event
			 * @type {object}
			 * @property {string} method - 'toggleVideoPlay' function call on PANOLENS.Viewer
			 */
      scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'toggleVideoPlay', data: !this.paused })

      this.paused = !this.paused

      item.update()
    }

    item = this.createCustomItem({

      style: {

        float: 'left',
        backgroundImage: 'url("' + PANOLENS.DataImage.VideoPlay + '")'

      },

      onTap: onTap

    })

    item.paused = true

    item.update = function (paused) {
      this.paused = paused !== undefined ? paused : this.paused

      this.style.backgroundImage = 'url("' + (this.paused
        ? PANOLENS.DataImage.VideoPlay
        : PANOLENS.DataImage.VideoPause) + '")'
    }

    return item
  }

  /**
	 * Create video seekbar
	 * @return {HTMLSpanElement} - The dom element icon for video seekbar
	 * @fires PANOLENS.Widget#panolens-viewer-handler
	 */
  PANOLENS.Widget.prototype.createVideoControlSeekbar = function () {
    var scope = this, item, progressElement, progressElementControl,
      isDragging = false, mouseX, percentageNow, percentageNext

    progressElement = document.createElement('div')
    progressElement.style.width = '0%'
    progressElement.style.height = '100%'
    progressElement.style.backgroundColor = '#fff'

    progressElementControl = document.createElement('div')
    progressElementControl.style.float = 'right'
    progressElementControl.style.width = '14px'
    progressElementControl.style.height = '14px'
    progressElementControl.style.transform = 'translate(7px, -5px)'
    progressElementControl.style.borderRadius = '50%'
    progressElementControl.style.backgroundColor = '#ddd'

    progressElementControl.addEventListener('mousedown', onMouseDown, false)
    progressElementControl.addEventListener('touchstart', onMouseDown, false)

    function onMouseDown (event) {
      event.stopPropagation()

      isDragging = true

      mouseX = event.clientX || (event.changedTouches && event.changedTouches[0].clientX)

      percentageNow = parseInt(progressElement.style.width) / 100

      addControlListeners()
    }

    function onVideoControlDrag (event) {
      var clientX

      if (isDragging) {
        clientX = event.clientX || (event.changedTouches && event.changedTouches[0].clientX)

        percentageNext = (clientX - mouseX) / item.clientWidth

        percentageNext = percentageNow + percentageNext

        percentageNext = percentageNext > 1 ? 1 : ((percentageNext < 0) ? 0 : percentageNext)

        item.setProgress(percentageNext)

        /**
				 * Viewer handler event
				 * @type {object}
				 * @property {string} method - 'setVideoCurrentTime' function call on PANOLENS.Viewer
				 * @property {number} data - Percentage of current video. Range from 0.0 to 1.0
				 */
        scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'setVideoCurrentTime', data: percentageNext })
      }
    }

    function onVideoControlStop (event) {
      event.stopPropagation()

      isDragging = false

      removeControlListeners()
    }

    function addControlListeners () {
      scope.container.addEventListener('mousemove', onVideoControlDrag, false)
      scope.container.addEventListener('mouseup', onVideoControlStop, false)
      scope.container.addEventListener('touchmove', onVideoControlDrag, false)
      scope.container.addEventListener('touchend', onVideoControlStop, false)
    }

    function removeControlListeners () {
      scope.container.removeEventListener('mousemove', onVideoControlDrag, false)
      scope.container.removeEventListener('mouseup', onVideoControlStop, false)
      scope.container.removeEventListener('touchmove', onVideoControlDrag, false)
      scope.container.removeEventListener('touchend', onVideoControlStop, false)
    }

    function onTap (event) {
      event.preventDefault()
      event.stopPropagation()

      var percentage

      if (event.target === progressElementControl) { return }

      percentage = (event.changedTouches && event.changedTouches.length > 0)
        ? (event.changedTouches[0].pageX - event.target.getBoundingClientRect().left) / this.clientWidth
        : event.offsetX / this.clientWidth

      /**
			 * Viewer handler event
			 * @type {object}
			 * @property {string} method - 'setVideoCurrentTime' function call on PANOLENS.Viewer
			 * @property {number} data - Percentage of current video. Range from 0.0 to 1.0
			 */
      scope.dispatchEvent({ type: 'panolens-viewer-handler', method: 'setVideoCurrentTime', data: percentage })

      item.setProgress(event.offsetX / this.clientWidth)
    }

    function onDispose () {
      removeControlListeners()
      progressElement = null
      progressElementControl = null
    }

    progressElement.appendChild(progressElementControl)

    item = this.createCustomItem({

      style: {

        float: 'left',
        width: '30%',
        height: '4px',
        marginTop: '20px',
        backgroundColor: 'rgba(188,188,188,0.8)'

      },

      onTap: onTap,
      onDispose: onDispose

    })

    item.appendChild(progressElement)

    item.setProgress = function (percentage) {
      progressElement.style.width = percentage * 100 + '%'
    }

    this.addEventListener('video-update', function (event) {
      item.setProgress(event.percentage)
    })

    return item
  }

  /**
	 * Create menu item
	 * @param  {string} title - Title to display
	 * @return {HTMLDomElement} - An anchor tag element
	 */
  PANOLENS.Widget.prototype.createMenuItem = function (title) {
    var scope = this, item = document.createElement('a')
    item.textContent = title
    item.style.display = 'block'
    item.style.padding = '10px'
    item.style.textDecoration = 'none'
    item.style.cursor = 'pointer'
    item.style.pointerEvents = 'auto'
    item.style.transition = this.DEFAULT_TRANSITION

    item.slide = function (right) {
      this.style.transform = 'translateX(' + (right ? '' : '-') + '100%)'
    }

    item.unslide = function () {
      this.style.transform = 'translateX(0)'
    }

    item.setIcon = function (url) {
      if (this.icon) {
        this.icon.style.backgroundImage = 'url(' + url + ')'
      }
    }

    item.setSelectionTitle = function (title) {
      if (this.selection) {
        this.selection.textContent = title
      }
    }

    item.addSelection = function (name) {
      var selection = document.createElement('span')
      selection.style.fontSize = '13px'
      selection.style.fontWeight = '300'
      selection.style.float = 'right'

      this.selection = selection
      this.setSelectionTitle(name)
      this.appendChild(selection)

      return this
    }

    item.addIcon = function (url, left, flip) {
      url = url || PANOLENS.DataImage.ChevronRight
      left = left || false
      flip = flip || false

      var element = document.createElement('span')
      element.style.float = left ? 'left' : 'right'
      element.style.width = '17px'
      element.style.height = '17px'
      element.style[ 'margin' + (left ? 'Right' : 'Left') ] = '12px'
      element.style.backgroundSize = 'cover'

      if (flip) {
        element.style.transform = 'rotateZ(180deg)'
      }

      this.icon = element
      this.setIcon(url)
      this.appendChild(element)

      return this
    }

    item.addSubMenu = function (title, items) {
      this.subMenu = scope.createSubMenu(title, items)

      return this
    }

    item.addEventListener('mouseenter', function () {
      this.style.backgroundColor = '#e0e0e0'
    }, false)

    item.addEventListener('mouseleave', function () {
      this.style.backgroundColor = '#fafafa'
    }, false)

    return item
  }

  /**
	 * Create menu item header
	 * @param  {string} title - Title to display
	 * @return {HTMLDomElement} - An anchor tag element
	 */
  PANOLENS.Widget.prototype.createMenuItemHeader = function (title) {
    var header = this.createMenuItem(title)

    header.style.borderBottom = '1px solid #333'
    header.style.paddingBottom = '15px'

    return header
  }

  /**
	 * Create main menu
	 * @param  {array} menus - Menu array list
	 * @return {HTMLDomElement} - A span element
	 */
  PANOLENS.Widget.prototype.createMainMenu = function (menus) {
    var scope = this, menu = this.createMenu(), subMenu

    menu._width = 200
    menu.changeSize(menu._width)

    function onTap (event) {
      event.preventDefault()
      event.stopPropagation()

      var mainMenu = scope.mainMenu, subMenu = this.subMenu

      function onNextTick () {
        mainMenu.changeSize(subMenu.clientWidth)
        subMenu.show()
        subMenu.unslideAll()
      }

      mainMenu.hide()
      mainMenu.slideAll()
      mainMenu.parentElement.appendChild(subMenu)

      scope.activeMainItem = this
      scope.activeSubMenu = subMenu

      window.requestAnimationFrame(onNextTick)
    }

    for (var i = 0; i < menus.length; i++) {
      var item = menu.addItem(menus[ i ].title)

      item.style.paddingLeft = '20px'

      item.addIcon()
        .addEventListener(scope.TOUCH_ENABLED ? 'touchend' : 'click', onTap, false)

      if (menus[ i ].subMenu && menus[ i ].subMenu.length > 0) {
        var title = menus[ i ].subMenu[ 0 ].title

        item.addSelection(title)
          .addSubMenu(menus[ i ].title, menus[ i ].subMenu)
      }
    }

    return menu
  }

  /**
	 * Create sub menu
	 * @param {string} title - Sub menu title
	 * @param {array} items - Item array list
	 * @return {HTMLDomElement} - A span element
	 */
  PANOLENS.Widget.prototype.createSubMenu = function (title, items) {
    var scope = this, menu, subMenu = this.createMenu()

    subMenu.items = items
    subMenu.activeItem

    function onTap (event) {
      event.preventDefault()
      event.stopPropagation()

      menu = scope.mainMenu
      menu.changeSize(menu._width)
      menu.unslideAll()
      menu.show()
      subMenu.slideAll(true)
      subMenu.hide()

      if (this.type !== 'header') {
        subMenu.setActiveItem(this)
        scope.activeMainItem.setSelectionTitle(this.textContent)

        this.handler && this.handler()
      }
    }

    subMenu.addHeader(title).addIcon(undefined, true, true).addEventListener(scope.TOUCH_ENABLED ? 'touchend' : 'click', onTap, false)

    for (var i = 0; i < items.length; i++) {
      var item = subMenu.addItem(items[ i ].title)

      item.style.fontWeight = 300
      item.handler = items[ i ].handler
      item.addIcon(' ', true)
      item.addEventListener(scope.TOUCH_ENABLED ? 'touchend' : 'click', onTap, false)

      if (!subMenu.activeItem) {
        subMenu.setActiveItem(item)
      }
    }

    subMenu.slideAll(true)

    return subMenu
  }

  /**
	 * Create general menu
	 * @return {HTMLDomElement} - A span element
	 */
  PANOLENS.Widget.prototype.createMenu = function () {
    var scope = this, menu = document.createElement('span'), style

    style = menu.style

    style.padding = '5px 0'
    style.position = 'fixed'
    style.bottom = '100%'
    style.right = '14px'
    style.backgroundColor = '#fafafa'
    style.fontFamily = 'Helvetica Neue'
    style.fontSize = '14px'
    style.visibility = 'hidden'
    style.opacity = 0
    style.boxShadow = '0 0 12pt rgba(0,0,0,0.25)'
  		style.borderRadius = '2px'
    style.overflow = 'hidden'
    style.willChange = 'width, height, opacity'
    style.pointerEvents = 'auto'
    style.transition = this.DEFAULT_TRANSITION

    menu.visible = false

    menu.changeSize = function (width, height) {
      if (width) {
        this.style.width = width + 'px'
      }

      if (height) {
        this.style.height = height + 'px'
      }
    }

    menu.show = function () {
      this.style.opacity = 1
      this.style.visibility = 'visible'
      this.visible = true
    }

    menu.hide = function () {
      this.style.opacity = 0
      this.style.visibility = 'hidden'
      this.visible = false
    }

    menu.toggle = function () {
      if (this.visible) {
        this.hide()
      } else {
        this.show()
      }
    }

    menu.slideAll = function (right) {
      for (var i = 0; i < menu.children.length; i++) {
        if (menu.children[ i ].slide) {
          menu.children[ i ].slide(right)
        }
      }
    }

    menu.unslideAll = function () {
      for (var i = 0; i < menu.children.length; i++) {
        if (menu.children[ i ].unslide) {
          menu.children[ i ].unslide()
        }
      }
    }

    menu.addHeader = function (title) {
      var header = scope.createMenuItemHeader(title)
      header.type = 'header'

      this.appendChild(header)

      return header
    }

    menu.addItem = function (title) {
      var item = scope.createMenuItem(title)
      item.type = 'item'

      this.appendChild(item)

      return item
    }

    menu.setActiveItem = function (item) {
      if (this.activeItem) {
        this.activeItem.setIcon(' ')
      }

      item.setIcon(PANOLENS.DataImage.Check)

      this.activeItem = item
    }

    menu.addEventListener('mousemove', this.PREVENT_EVENT_HANDLER, true)
    menu.addEventListener('mouseup', this.PREVENT_EVENT_HANDLER, true)
    menu.addEventListener('mousedown', this.PREVENT_EVENT_HANDLER, true)

    return menu
  }

  /**
	 * Create custom item element
	 * @return {HTMLSpanElement} - The dom element icon
	 */
  PANOLENS.Widget.prototype.createCustomItem = function (options) {
    options = options || {}

    var scope = this,
      item = options.element || document.createElement('span')

    item.style.cursor = 'pointer'
    item.style.float = 'right'
    item.style.width = '44px'
    item.style.height = '100%'
    item.style.backgroundSize = '60%'
    item.style.backgroundRepeat = 'no-repeat'
    item.style.backgroundPosition = 'center'
    item.style.webkitUserSelect =
		item.style.MozUserSelect =
		item.style.userSelect = 'none'
    item.style.position = 'relative'
    item.style.pointerEvents = 'auto'

    // White glow on icon
    item.addEventListener(scope.TOUCH_ENABLED ? 'touchstart' : 'mouseenter', function () {
      item.style.filter =
			item.style.webkitFilter = 'drop-shadow(0 0 5px rgba(255,255,255,1))'
    })
    item.addEventListener(scope.TOUCH_ENABLED ? 'touchend' : 'mouseleave', function () {
      item.style.filter =
			item.style.webkitFilter = ''
    })

    item = this.mergeStyleOptions(item, options.style)

    if (options.onTap) {
      item.addEventListener(scope.TOUCH_ENABLED ? 'touchend' : 'click', options.onTap, false)
    }

    item.dispose = function () {
      item.removeEventListener(scope.TOUCH_ENABLED ? 'touchend' : 'click', options.onTap, false)

      options.onDispose && options.onDispose()
    }

    return item
  }

  /**
	 * Merge item css style
	 * @param  {HTMLDOMElement} element - The element to be merged with style
	 * @param  {object} options - The style options
	 * @return {HTMLDOMElement} - The same element with merged styles
	 */
  PANOLENS.Widget.prototype.mergeStyleOptions = function (element, options) {
    options = options || {}

    for (var property in options) {
      if (options.hasOwnProperty(property)) {
        element.style[ property ] = options[ property ]
      }
    }

    return element
  }

  /**
	 * Dispose widgets by detaching dom elements from container
	 */
  PANOLENS.Widget.prototype.dispose = function () {
    if (this.barElement) {
      this.container.removeChild(this.barElement)
      this.barElement.dispose()
      this.barElement = null
    }
  }
})(); (function () {
  /**
	 * Information spot attached to panorama
	 * @constructor
	 * @param {number} [scale=300] - Infospot scale
	 * @param {imageSrc} [imageSrc=PANOLENS.DataImage.Info] - Image overlay info
	 * @param {boolean} [animated=true] - Enable default hover animation
	 */
  PANOLENS.Infospot = function (scale, imageSrc, animated) {
    var scope = this, ratio, startScale, endScale, duration

    scale = scale || 300
    imageSrc = imageSrc || PANOLENS.DataImage.Info
    duration = 500

    THREE.Sprite.call(this)

    this.type = 'infospot'

    this.animated = animated !== undefined ? animated : true
    this.isHovering = false
    this.visible = false

    this.element
    this.toPanorama
    this.cursorStyle

    this.mode = PANOLENS.Modes.UNKNOWN

    this.scale.set(scale, scale, 1)
    this.rotation.y = Math.PI
    this.scaleFactor = 1.3

    this.container

    // Event Handler
    this.HANDLER_FOCUS

    PANOLENS.Utils.TextureLoader.load(imageSrc, postLoad)

    function postLoad (texture) {
      texture.wrapS = THREE.RepeatWrapping
      texture.repeat.x = -1

      texture.image.width = texture.image.naturalWidth || 64
      texture.image.height = texture.image.naturalHeight || 64

      ratio = texture.image.width / texture.image.height
      scope.scale.set(ratio * scale, scale, 1)

      startScale = scope.scale.clone()

      scope.scaleUpAnimation = new TWEEN.Tween(scope.scale)
        .to({ x: startScale.x * scope.scaleFactor, y: startScale.y * scope.scaleFactor }, duration)
        .easing(TWEEN.Easing.Elastic.Out)

      scope.scaleDownAnimation = new TWEEN.Tween(scope.scale)
        .to({ x: startScale.x, y: startScale.y }, duration)
        .easing(TWEEN.Easing.Elastic.Out)

      scope.material.side = THREE.DoubleSide
      scope.material.map = texture
      scope.material.depthTest = false
      scope.material.needsUpdate = true
    }

    function show () {
      this.visible = true
    }

    function hide () {
      this.visible = false
    }

    // Add show and hide animations
    this.showAnimation = new TWEEN.Tween(this.material)
      .to({ opacity: 1 }, duration)
      .onStart(show.bind(this))
      .easing(TWEEN.Easing.Quartic.Out)

    this.hideAnimation = new TWEEN.Tween(this.material)
      .to({ opacity: 0 }, duration)
      .onComplete(hide.bind(this))
      .easing(TWEEN.Easing.Quartic.Out)

    // Attach event listeners
    this.addEventListener('click', this.onClick)
    this.addEventListener('hover', this.onHover)
    this.addEventListener('hoverenter', this.onHoverStart)
    this.addEventListener('hoverleave', this.onHoverEnd)
    this.addEventListener('panolens-dual-eye-effect', this.onDualEyeEffect)
    this.addEventListener('panolens-container', this.setContainer.bind(this))
    this.addEventListener('dismiss', this.onDismiss)
    this.addEventListener('panolens-infospot-focus', this.setFocusMethod)
  }

  PANOLENS.Infospot.prototype = Object.create(THREE.Sprite.prototype)

  /**
	 * Set infospot container
	 * @param {HTMLElement|object} data - Data with container information
	 */
  PANOLENS.Infospot.prototype.setContainer = function (data) {
    var container

    if (data instanceof HTMLElement) {
      container = data
    } else if (data && data.container) {
      container = data.container
    }

    // Append element if exists
    if (container && this.element) {
      container.appendChild(this.element)
    }

    this.container = container
  }

  /**
	 * Get container
	 * @return {HTMLElement} - The container of this infospot
	 */
  PANOLENS.Infospot.prototype.getContainer = function () {
    return this.container
  }

  /**
	 * This will be called by a click event
	 * Translate and lock the hovering element if any
	 * @param  {object} event - Event containing mouseEvent with clientX and clientY
	 */
  PANOLENS.Infospot.prototype.onClick = function (event) {
    if (this.element && this.getContainer()) {
      this.onHoverStart(event)

      // Lock element
      this.lockHoverElement()
    }
  }

  /**
	 * Dismiss current element if any
	 * @param  {object} event - Dismiss event
	 */
  PANOLENS.Infospot.prototype.onDismiss = function (event) {
    if (this.element) {
      this.unlockHoverElement()
      this.onHoverEnd()
    }
  }

  /**
	 * This will be called by a mouse hover event
	 * Translate the hovering element if any
	 * @param  {object} event - Event containing mouseEvent with clientX and clientY
	 */
  PANOLENS.Infospot.prototype.onHover = function (event) {

  }

  /**
	 * This will be called on a mouse hover start
	 * Sets cursor style to 'pointer', display the element and scale up the infospot
	 */
  PANOLENS.Infospot.prototype.onHoverStart = function (event) {
    if (!this.getContainer()) { return }

    var cursorStyle = this.cursorStyle || (this.mode === PANOLENS.Modes.NORMAL ? 'pointer' : 'default')

    this.isHovering = true
    this.container.style.cursor = cursorStyle

    if (this.animated) {
      this.scaleDownAnimation && this.scaleDownAnimation.stop()
      this.scaleUpAnimation && this.scaleUpAnimation.start()
    }

    if (this.element && event.mouseEvent.clientX >= 0 && event.mouseEvent.clientY >= 0) {
      if (this.mode === PANOLENS.Modes.CARDBOARD || this.mode === PANOLENS.Modes.STEREO) {
        this.element.style.display = 'none'
        this.element.left && (this.element.left.style.display = 'block')
        this.element.right && (this.element.right.style.display = 'block')

        // Store element width for reference
        this.element._width = this.element.left.clientWidth
        this.element._height = this.element.left.clientHeight
      } else {
        this.element.style.display = 'block'
        this.element.left && (this.element.left.style.display = 'none')
        this.element.right && (this.element.right.style.display = 'none')

        // Store element width for reference
        this.element._width = this.element.clientWidth
        this.element._height = this.element.clientHeight
      }
    }
  }

  /**
	 * This will be called on a mouse hover end
	 * Sets cursor style to 'default', hide the element and scale down the infospot
	 */
  PANOLENS.Infospot.prototype.onHoverEnd = function () {
    if (!this.getContainer()) { return }

    this.isHovering = false
    this.container.style.cursor = 'default'

    if (this.animated) {
      this.scaleUpAnimation && this.scaleUpAnimation.stop()
      this.scaleDownAnimation && this.scaleDownAnimation.start()
    }

    if (this.element && !this.element.locked) {
      this.element.style.display = 'none'
      this.element.left && (this.element.left.style.display = 'none')
      this.element.right && (this.element.right.style.display = 'none')

      this.unlockHoverElement()
    }
  }

  /**
	 * On dual eye effect handler
	 * Creates duplicate left and right element
	 * @param  {object} event - panolens-dual-eye-effect event
	 */
  PANOLENS.Infospot.prototype.onDualEyeEffect = function (event) {
    if (!this.getContainer()) { return }

    var element, halfWidth, halfHeight

    this.mode = event.mode

    element = this.element

    halfWidth = this.container.clientWidth / 2
    halfHeight = this.container.clientHeight / 2

    if (!element) {
      return
    }

    if (!element.left || !element.right) {
      element.left = element.cloneNode(true)
      element.right = element.cloneNode(true)
    }

    if (this.mode === PANOLENS.Modes.CARDBOARD || this.mode === PANOLENS.Modes.STEREO) {
      element.left.style.display = element.style.display
      element.right.style.display = element.style.display
      element.style.display = 'none'
    } else {
      element.style.display = element.left.style.display
      element.left.style.display = 'none'
      element.right.style.display = 'none'
    }

    // Update elements translation
    this.translateElement(halfWidth, halfHeight)

    this.container.appendChild(element.left)
    this.container.appendChild(element.right)
  }

  /**
	 * Translate the hovering element by css transform
	 * @param  {number} x - X position on the window screen
	 * @param  {number} y - Y position on the window screen
	 */
  PANOLENS.Infospot.prototype.translateElement = function (x, y) {
    if (!this.element._width || !this.element._height || !this.getContainer()) {
      return
    }

    var left, top, element, width, height, delta, container

    container = this.container
    element = this.element
    width = element._width / 2
    height = element._height / 2
    delta = element.verticalDelta !== undefined ? element.verticalDelta : 40

    left = x - width
    top = y - height - delta

    if ((this.mode === PANOLENS.Modes.CARDBOARD || this.mode === PANOLENS.Modes.STEREO) &&
				element.left && element.right &&
				!(x === container.clientWidth / 2 && y === container.clientHeight / 2)) {
      left = container.clientWidth / 4 - width + (x - container.clientWidth / 2)
      top = container.clientHeight / 2 - height - delta + (y - container.clientHeight / 2)

      this.setElementStyle('transform', element.left, 'translate(' + left + 'px, ' + top + 'px)')

      left += container.clientWidth / 2

      this.setElementStyle('transform', element.right, 'translate(' + left + 'px, ' + top + 'px)')
    } else {
      this.setElementStyle('transform', element, 'translate(' + left + 'px, ' + top + 'px)')
    }
  }

  /**
	 * Set vendor specific css
	 * @param {string} type - CSS style name
	 * @param {HTMLElement} element - The element to be modified
	 * @param {string} value - Style value
	 */
  PANOLENS.Infospot.prototype.setElementStyle = function (type, element, value) {
    var style = element.style

    if (type === 'transform') {
      style.webkitTransform = style.msTransform = style.transform = value
    }
  }

  /**
	 * Set hovering text content
	 * @param {string} text - Text to be displayed
	 */
  PANOLENS.Infospot.prototype.setText = function (text) {
    if (this.element) {
      this.element.textContent = text
    }
  }

  /**
	 * Set cursor css style on hover
	 */
  PANOLENS.Infospot.prototype.setCursorHoverStyle = function (style) {
    this.cursorStyle = style
  }

  /**
	 * Add hovering text element
	 * @param {string} text - Text to be displayed
	 * @param {number} [delta=40] - Vertical delta to the infospot
	 */
  PANOLENS.Infospot.prototype.addHoverText = function (text, delta) {
    if (!this.element) {
      this.element = document.createElement('div')
      this.element.style.display = 'none'
      this.element.style.color = '#fff'
      this.element.style.top = 0
      this.element.style.maxWidth = '50%'
      this.element.style.maxHeight = '50%'
      this.element.style.textShadow = '0 0 3px #000000'
      this.element.style.fontFamily = '"Trebuchet MS", Helvetica, sans-serif'
      this.element.style.position = 'absolute'
      this.element.classList.add('panolens-infospot')
      this.element.verticalDelta = delta !== undefined ? delta : 40
    }

    this.setText(text)
  }

  /**
	 * Add hovering element by cloning an element
	 * @param {HTMLDOMElement} el - Element to be cloned and displayed
	 * @param {number} [delta=40] - Vertical delta to the infospot
	 */
  PANOLENS.Infospot.prototype.addHoverElement = function (el, delta) {
    if (!this.element) {
      this.element = el.cloneNode(true)
      this.element.style.display = 'none'
      this.element.style.top = 0
      this.element.style.position = 'absolute'
      this.element.classList.add('panolens-infospot')
      this.element.verticalDelta = delta !== undefined ? delta : 40
    }
  }

  /**
	 * Remove hovering element
	 */
  PANOLENS.Infospot.prototype.removeHoverElement = function () {
    if (this.element) {
      if (this.element.left) {
        this.container.removeChild(this.element.left)
        this.element.left = null
      }

      if (this.element.right) {
        this.container.removeChild(this.element.right)
        this.element.right = null
      }

      this.container.removeChild(this.element)
      this.element = null
    }
  }

  /**
	 * Lock hovering element
	 */
  PANOLENS.Infospot.prototype.lockHoverElement = function () {
    if (this.element) {
      this.element.locked = true
    }
  }

  /**
	 * Unlock hovering element
	 */
  PANOLENS.Infospot.prototype.unlockHoverElement = function () {
    if (this.element) {
      this.element.locked = false
    }
  }

  /**
	 * Show infospot
	 * @param  {number} [delay=0] - Delay time to show
	 */
  PANOLENS.Infospot.prototype.show = function (delay) {
    delay = delay || 0

    if (this.animated) {
      this.hideAnimation && this.hideAnimation.stop()
      this.showAnimation && this.showAnimation.delay(delay).start()
    }
  }

  /**
	 * Hide infospot
	 * @param  {number} [delay=0] - Delay time to hide
	 */
  PANOLENS.Infospot.prototype.hide = function (delay) {
    delay = delay || 0

    if (this.animated) {
      this.showAnimation && this.showAnimation.stop()
      this.hideAnimation && this.hideAnimation.delay(delay).start()
    }
  }

  /**
	 * Set focus event handler
	 */
  PANOLENS.Infospot.prototype.setFocusMethod = function (event) {
    if (event) {
      this.HANDLER_FOCUS = event.method
    }
  }

  /**
	 * Focus camera center to this infospot
	 * @param {number} [duration=1000] - Duration to tween
	 * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
	 */
  PANOLENS.Infospot.prototype.focus = function (duration, easing) {
    if (this.HANDLER_FOCUS) {
      this.HANDLER_FOCUS(this.position, duration, easing)
      this.onDismiss()
    }
  }

  /**
	 * Dispose infospot
	 */
  PANOLENS.Infospot.prototype.dispose = function () {
    this.removeHoverElement()
    this.material.dispose()

    if (this.parent) {
      this.parent.remove(this)
    }
  }
})(); (function () {
  'use strict'

  /**
	 * Viewer contains pre-defined scene, camera and renderer
	 * @constructor
	 * @param {object} [options] - Use custom or default config options
	 * @param {HTMLElement} [options.container] - A HTMLElement to host the canvas
	 * @param {THREE.Scene} [options.scene=THREE.Scene] - A THREE.Scene which contains panorama and 3D objects
	 * @param {THREE.Camera} [options.camera=THREE.PerspectiveCamera] - A THREE.Camera to view the scene
	 * @param {THREE.WebGLRenderer} [options.renderer=THREE.WebGLRenderer] - A THREE.WebGLRenderer to render canvas
	 * @param {boolean} [options.controlBar=true] - Show/hide control bar on the bottom of the container
	 * @param {array}   [options.controlButtons=[]] - Button names to mount on controlBar if controlBar exists, Defaults to ['fullscreen', 'setting', 'video']
	 * @param {boolean} [options.autoHideControlBar=false] - Auto hide control bar when click on non-active area
	 * @param {boolean} [options.autoHideInfospot=true] - Auto hide infospots when click on non-active area
	 * @param {boolean} [options.horizontalView=false] - Allow only horizontal camera control
	 * @param {number}  [options.clickTolerance=10] - Distance tolerance to tigger click / tap event
	 * @param {number}  [options.cameraFov=60] - Camera field of view value
	 * @param {boolean} [options.reverseDragging=false] - Reverse dragging direction
	 * @param {boolean} [options.enableReticle=false] - Enable reticle for mouseless interaction other than VR mode
	 * @param {number}  [options.dwellTime=1500] - Dwell time for reticle selection
	 * @param {boolean} [options.autoReticleSelect=true] - Auto select a clickable target after dwellTime
	 * @param {boolean} [options.viewIndicator=false] - Adds an angle view indicator in upper left corner
	 * @param {number}  [options.indicatorSize=30] - Size of View Indicator
	 * @param {string}  [options.output='none'] - Whether and where to output raycast position. Could be 'console' or 'overlay'
	 */
  PANOLENS.Viewer = function (options) {
    THREE.EventDispatcher.call(this)

    if (!THREE) {
      console.error('Three.JS not found')

      return
    }

    var container

    options = options || {}
    options.controlBar = options.controlBar !== undefined ? options.controlBar : true
    options.controlButtons = options.controlButtons || [ 'fullscreen', 'setting', 'video' ]
    options.autoHideControlBar = options.autoHideControlBar !== undefined ? options.autoHideControlBar : false
    options.autoHideInfospot = options.autoHideInfospot !== undefined ? options.autoHideInfospot : true
    options.horizontalView = options.horizontalView !== undefined ? options.horizontalView : false
    options.clickTolerance = options.clickTolerance || 10
    options.cameraFov = options.cameraFov || 60
    options.reverseDragging = options.reverseDragging || false
    options.enableReticle = options.enableReticle || false
    options.dwellTime = options.dwellTime || 1500
    options.autoReticleSelect = options.autoReticleSelect !== undefined ? options.autoReticleSelect : true
    options.viewIndicator = options.viewIndicator !== undefined ? options.viewIndicator : false
    options.indicatorSize = options.indicatorSize || 30
    options.output = options.output ? options.output : 'none'

    this.options = options

    // Container
    if (options.container) {
      container = options.container
      container._width = container.clientWidth
      container._height = container.clientHeight
    } else {
      container = document.createElement('div')
      container.classList.add('panolens-container')
      container.style.width = '100%'
      container.style.height = '100%'
      container._width = window.innerWidth
      container._height = window.innerHeight
      document.body.appendChild(container)
    }

    this.container = container

    this.camera = options.camera || new THREE.PerspectiveCamera(this.options.cameraFov, this.container.clientWidth / this.container.clientHeight, 1, 10000)
    this.scene = options.scene || new THREE.Scene()
    this.renderer = options.renderer || new THREE.WebGLRenderer({ alpha: true, antialias: false })

    this.viewIndicatorSize = options.indicatorSize

    this.reticle = {}
    this.tempEnableReticle = this.options.enableReticle

    this.mode = PANOLENS.Modes.NORMAL

    this.OrbitControls
    this.DeviceOrientationControls

    this.CardboardEffect
    this.StereoEffect

    this.controls
    this.effect
    this.panorama
    this.widget

    this.hoverObject
    this.infospot
    this.pressEntityObject
    this.pressObject

    this.raycaster = new THREE.Raycaster()
    this.raycasterPoint = new THREE.Vector2()
    this.userMouse = new THREE.Vector2()
    this.updateCallbacks = []
    this.requestAnimationId

    this.cameraFrustum = new THREE.Frustum()
    this.cameraViewProjectionMatrix = new THREE.Matrix4()

    this.outputDivElement

    // Handler references
    this.HANDLER_MOUSE_DOWN = this.onMouseDown.bind(this)
    this.HANDLER_MOUSE_UP = this.onMouseUp.bind(this)
    this.HANDLER_MOUSE_MOVE = this.onMouseMove.bind(this)
    this.HANDLER_WINDOW_RESIZE = this.onWindowResize.bind(this)
    this.HANDLER_KEY_DOWN = this.onKeyDown.bind(this)
    this.HANDLER_KEY_UP = this.onKeyUp.bind(this)
    this.HANDLER_TAP = this.onTap.bind(this, {
      clientX: this.container.clientWidth / 2,
      clientY: this.container.clientHeight / 2
    })

    // Flag for infospot output
    this.OUTPUT_INFOSPOT = false

    // Animations
    this.tweenLeftAnimation = new TWEEN.Tween()
    this.tweenUpAnimation = new TWEEN.Tween()

    // Renderer
    this.renderer.setPixelRatio(window.devicePixelRatio)
    this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
    this.renderer.setClearColor(0x000000, 1)
    this.renderer.sortObjects = false

    // Append Renderer Element to container
    this.renderer.domElement.classList.add('panolens-canvas')
    this.renderer.domElement.style.display = 'block'
    this.container.style.backgroundColor = '#000'
    this.container.appendChild(this.renderer.domElement)

    // Camera Controls
    this.OrbitControls = new THREE.OrbitControls(this.camera, this.container)
    this.OrbitControls.name = 'orbit'
    this.OrbitControls.minDistance = 1
    this.OrbitControls.noPan = true
    this.DeviceOrientationControls = new THREE.DeviceOrientationControls(this.camera, this.container)
    this.DeviceOrientationControls.name = 'device-orientation'
    this.DeviceOrientationControls.enabled = false
    this.camera.position.z = 1

    // Register change event if passiveRenering
    if (this.options.passiveRendering) {
      console.warn('passiveRendering is now deprecated')
    }

    // Controls
    this.controls = [ this.OrbitControls, this.DeviceOrientationControls ]
    this.control = this.OrbitControls

    // Cardboard effect
    this.CardboardEffect = new THREE.CardboardEffect(this.renderer)
    this.CardboardEffect.setSize(this.container.clientWidth, this.container.clientHeight)

    // Stereo effect
    this.StereoEffect = new THREE.StereoEffect(this.renderer)
    this.StereoEffect.setSize(this.container.clientWidth, this.container.clientHeight)

    this.effect = this.CardboardEffect

    // Add default hidden reticle
    this.addReticle()

    // Lock horizontal view
    if (this.options.horizontalView) {
      this.OrbitControls.minPolarAngle = Math.PI / 2
      this.OrbitControls.maxPolarAngle = Math.PI / 2
    }

    // Add Control UI
    if (this.options.controlBar !== false) {
      this.addDefaultControlBar(this.options.controlButtons)
    }

    // Add View Indicator
    if (this.options.viewIndicator) {
      this.addViewIndicator()
    }

    // Reverse dragging direction
    if (this.options.reverseDragging) {
      this.reverseDraggingDirection()
    }

    // Register event if reticle is enabled, otherwise defaults to mouse
    if (this.options.enableReticle) {
      this.enableReticleControl()
    } else {
      this.registerMouseAndTouchEvents()
    }

    if (this.options.output === 'overlay') {
      this.addOutputElement()
    }

    // Register dom event listeners
    this.registerEventListeners()

    // Animate
    this.animate.call(this)
  }

  PANOLENS.Viewer.prototype = Object.create(THREE.EventDispatcher.prototype)

  PANOLENS.Viewer.prototype.constructor = PANOLENS.Viewer

  /**
	 * Add an object to the scene
	 * Automatically hookup with panolens-viewer-handler listener
	 * to communicate with viewer method
	 * @param {THREE.Object3D} object - The object to be added
	 */
  PANOLENS.Viewer.prototype.add = function (object) {
    if (arguments.length > 1) {
      for (var i = 0; i < arguments.length; i++) {
        this.add(arguments[ i ])
      }

      return this
    }

    this.scene.add(object)

    // All object added to scene has 'panolens-viewer-handler' event to handle viewer communication
    if (object.addEventListener) {
      object.addEventListener('panolens-viewer-handler', this.eventHandler.bind(this))
    }

    // All object added to scene being passed with container
    if (object instanceof PANOLENS.Panorama && object.dispatchEvent) {
      object.dispatchEvent({ type: 'panolens-container', container: this.container })
    }

    // Hookup default panorama event listeners
    if (object.type === 'panorama') {
      this.addPanoramaEventListener(object)

      if (!this.panorama) {
        this.setPanorama(object)
      }
    }
  }

  /**
	 * Remove an object from the scene
	 * @param  {THREE.Object3D} object - Object to be removed
	 */
  PANOLENS.Viewer.prototype.remove = function (object) {
    if (object.removeEventListener) {
      object.removeEventListener('panolens-viewer-handler', this.eventHandler.bind(this))
    }

    this.scene.remove(object)
  }

  /**
	 * Add default control bar
	 * @param {array} array - The control buttons array
	 */
  PANOLENS.Viewer.prototype.addDefaultControlBar = function (array) {
    var scope = this

    if (this.widget) {
      console.warn('Default control bar exists')
      return
    }

    this.widget = new PANOLENS.Widget(this.container)
    this.widget.addEventListener('panolens-viewer-handler', this.eventHandler.bind(this))
    this.widget.addControlBar()
    array.forEach(function (buttonName) {
      scope.widget.addControlButton(buttonName)
    })
  }

  /**
	 * Set a panorama to be the current one
	 * @param {PANOLENS.Panorama} pano - Panorama to be set
	 */
  PANOLENS.Viewer.prototype.setPanorama = function (pano) {
    var scope = this, leavingPanorama = this.panorama

    if (pano.type === 'panorama' && leavingPanorama !== pano) {
      // Clear exisiting infospot
      this.hideInfospot()

      var afterEnterComplete = function () {
        leavingPanorama && leavingPanorama.onLeave()
        pano.removeEventListener('enter-fade-start', afterEnterComplete)
      }

      pano.addEventListener('enter-fade-start', afterEnterComplete);

      // Assign and enter panorama
      (this.panorama = pano).onEnter()
    }
  }

  /**
	 * Event handler to execute commands from child objects
	 * @param {object} event - The dispatched event with method as function name and data as an argument
	 */
  PANOLENS.Viewer.prototype.eventHandler = function (event) {
    if (event.method && this[ event.method ]) {
      this[ event.method ](event.data)
    }
  }

  /**
	 * Dispatch event to all descendants
	 * @param  {object} event - Event to be passed along
	 */
  PANOLENS.Viewer.prototype.dispatchEventToChildren = function (event) {
    this.scene.traverse(function (object) {
      if (object.dispatchEvent) {
        object.dispatchEvent(event)
      }
    })
  }

  /**
	 * Set widget content
	 * @param  {integer} controlIndex - Control index
	 * @param  {PANOLENS.Modes} mode - Modes for effects
	 */
  PANOLENS.Viewer.prototype.activateWidgetItem = function (controlIndex, mode) {
    var mainMenu = this.widget.mainMenu
    var ControlMenuItem = mainMenu.children[ 0 ]
    var ModeMenuItem = mainMenu.children[ 1 ]

    var item

    if (controlIndex !== undefined) {
      switch (controlIndex) {
        case 0:

          item = ControlMenuItem.subMenu.children[ 1 ]

          break

        case 1:

          item = ControlMenuItem.subMenu.children[ 2 ]

          break

        default:

          item = ControlMenuItem.subMenu.children[ 1 ]

          break
      }

      ControlMenuItem.subMenu.setActiveItem(item)
      ControlMenuItem.setSelectionTitle(item.textContent)
    }

    if (mode !== undefined) {
      switch (mode) {
        case PANOLENS.Modes.CARDBOARD:

          item = ModeMenuItem.subMenu.children[ 2 ]

          break

        case PANOLENS.Modes.STEREO:

          item = ModeMenuItem.subMenu.children[ 3 ]

          break

        default:

          item = ModeMenuItem.subMenu.children[ 1 ]

          break
      }

      ModeMenuItem.subMenu.setActiveItem(item)
      ModeMenuItem.setSelectionTitle(item.textContent)
    }
  }

  /**
	 * Enable rendering effect
	 * @param  {PANOLENS.Modes} mode - Modes for effects
	 */
  PANOLENS.Viewer.prototype.enableEffect = function (mode) {
    if (this.mode === mode) { return }
    if (mode === PANOLENS.Modes.NORMAL) { this.disableEffect(); return } else { this.mode = mode }

    var fov = this.camera.fov

    switch (mode) {
      case PANOLENS.Modes.CARDBOARD:

        this.effect = this.CardboardEffect
        this.enableReticleControl()

        break

      case PANOLENS.Modes.STEREO:

        this.effect = this.StereoEffect
        this.enableReticleControl()

        break

      default:

        this.effect = null
        this.disableReticleControl()

        break
    }

    this.activateWidgetItem(undefined, this.mode)

    /**
		 * Dual eye effect event
		 * @type {object}
		 * @event PANOLENS.Viewer#panolens-dual-eye-effect
		 * @event PANOLENS.Infospot#panolens-dual-eye-effect
		 * @property {PANOLENS.Modes} mode - Current display mode
		 */
    this.dispatchEventToChildren({ type: 'panolens-dual-eye-effect', mode: this.mode })

    // Force effect stereo camera to update by refreshing fov
    this.camera.fov = fov + 10e-3
    this.effect.setSize(this.container.clientWidth, this.container.clientHeight)
    this.render()
    this.camera.fov = fov
  }

  /**
	 * Disable additional rendering effect
	 */
  PANOLENS.Viewer.prototype.disableEffect = function () {
    if (this.mode === PANOLENS.Modes.NORMAL) { return }

    this.mode = PANOLENS.Modes.NORMAL
    this.disableReticleControl()

    this.activateWidgetItem(undefined, this.mode)

    /**
		 * Dual eye effect event
		 * @type {object}
		 * @event PANOLENS.Viewer#panolens-dual-eye-effect
		 * @event PANOLENS.Infospot#panolens-dual-eye-effect
		 * @property {PANOLENS.Modes} mode - Current display mode
		 */
    this.dispatchEventToChildren({ type: 'panolens-dual-eye-effect', mode: this.mode })

    this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
    this.render()
  }

  /**
	 * Enable reticle control
	 */
  PANOLENS.Viewer.prototype.enableReticleControl = function () {
    if (this.reticle.visible) { return }
    if (!this.reticle.textureLoaded) { this.reticle.loadTextures() }

    this.tempEnableReticle = true

    // Register reticle event and unregister mouse event
    this.unregisterMouseAndTouchEvents()
    this.reticle.show()
    this.registerReticleEvent()
    this.updateReticleEvent()
  }

  /**
	 * Disable reticle control
	 */
  PANOLENS.Viewer.prototype.disableReticleControl = function () {
    this.tempEnableReticle = false

    // Register mouse event and unregister reticle event
    if (!this.options.enableReticle) {
      this.reticle.hide()
      this.unregisterReticleEvent()
      this.registerMouseAndTouchEvents()
    } else {
      this.updateReticleEvent()
    }
  }

  /**
	 * Toggle video play or stop
	 * @fires PANOLENS.Viewer#video-toggle
	 */
  PANOLENS.Viewer.prototype.toggleVideoPlay = function (pause) {
    if (this.panorama instanceof PANOLENS.VideoPanorama) {
      /**
			 * Toggle video event
			 * @type {object}
			 * @event PANOLENS.Viewer#video-toggle
			 */
      this.panorama.dispatchEvent({ type: 'video-toggle', pause: pause })
    }
  }

  /**
	 * Set currentTime in a video
	 * @param {number} percentage - Percentage of a video. Range from 0.0 to 1.0
	 * @fires PANOLENS.Viewer#video-time
	 */
  PANOLENS.Viewer.prototype.setVideoCurrentTime = function (percentage) {
    if (this.panorama instanceof PANOLENS.VideoPanorama) {
      /**
			 * Setting video time event
			 * @type {object}
			 * @event PANOLENS.Viewer#video-time
			 * @property {number} percentage - Percentage of a video. Range from 0.0 to 1.0
			 */
      this.panorama.dispatchEvent({ type: 'video-time', percentage: percentage })
    }
  }

  /**
	 * This will be called when video updates if an widget is present
	 * @param {number} percentage - Percentage of a video. Range from 0.0 to 1.0
	 * @fires PANOLENS.Viewer#video-update
	 */
  PANOLENS.Viewer.prototype.onVideoUpdate = function (percentage) {
    /**
		 * Video update event
		 * @type {object}
		 * @event PANOLENS.Viewer#video-update
		 * @property {number} percentage - Percentage of a video. Range from 0.0 to 1.0
		 */
    this.widget && this.widget.dispatchEvent({ type: 'video-update', percentage: percentage })
  }

  /**
	 * Add update callback to be called every animation frame
	 */
  PANOLENS.Viewer.prototype.addUpdateCallback = function (fn) {
    if (fn) {
      this.updateCallbacks.push(fn)
    }
  }

  /**
	 * Remove update callback
	 * @param  {Function} fn - The function to be removed
	 */
  PANOLENS.Viewer.prototype.removeUpdateCallback = function (fn) {
    var index = this.updateCallbacks.indexOf(fn)

    if (fn && index >= 0) {
      this.updateCallbacks.splice(index, 1)
    }
  }

  /**
	 * Show video widget
	 */
  PANOLENS.Viewer.prototype.showVideoWidget = function () {
    /**
		 * Show video widget event
		 * @type {object}
		 * @event PANOLENS.Viewer#video-control-show
		 */
    this.widget && this.widget.dispatchEvent({ type: 'video-control-show' })
  }

  /**
	 * Hide video widget
	 */
  PANOLENS.Viewer.prototype.hideVideoWidget = function () {
    /**
		 * Hide video widget
		 * @type {object}
		 * @event PANOLENS.Viewer#video-control-hide
		 */
    this.widget && this.widget.dispatchEvent({ type: 'video-control-hide' })
  }

  PANOLENS.Viewer.prototype.updateVideoPlayButton = function (paused) {
    if (this.widget &&
			 this.widget.videoElement &&
			 this.widget.videoElement.controlButton) {
      this.widget.videoElement.controlButton.update(paused)
    }
  }

  /**
	 * Add default panorama event listeners
	 * @param {PANOLENS.Panorama} pano - The panorama to be added with event listener
	 */
  PANOLENS.Viewer.prototype.addPanoramaEventListener = function (pano) {
    var scope = this

    // Set camera control on every panorama
    pano.addEventListener('enter-fade-start', this.setCameraControl.bind(this))

    // Show and hide widget event only when it's PANOLENS.VideoPanorama
    if (pano instanceof PANOLENS.VideoPanorama) {
      pano.addEventListener('enter-fade-start', this.showVideoWidget.bind(this))
      pano.addEventListener('leave', function () {
        if (!(this.panorama instanceof PANOLENS.VideoPanorama)) {
          this.hideVideoWidget.call(this)
        }
      }.bind(this))
    }
  }

  /**
	 * Set camera control
	 */
  PANOLENS.Viewer.prototype.setCameraControl = function () {
    this.OrbitControls.target.copy(this.panorama.position)
  }

  /**
	 * Get current camera control
	 * @return {object} - Current navigation control. THREE.OrbitControls or THREE.DeviceOrientationControls
	 */
  PANOLENS.Viewer.prototype.getControl = function () {
    return this.control
  },

  /**
	 * Get scene
	 * @return {THREE.Scene} - Current scene which the viewer is built on
	 */
  PANOLENS.Viewer.prototype.getScene = function () {
    return this.scene
  }

  /**
	 * Get camera
	 * @return {THREE.Camera} - The scene camera
	 */
  PANOLENS.Viewer.prototype.getCamera = function () {
    return this.camera
  },

  /**
	 * Get renderer
	 * @return {THREE.WebGLRenderer} - The renderer using webgl
	 */
  PANOLENS.Viewer.prototype.getRenderer = function () {
    return this.renderer
  }

  /**
	 * Get container
	 * @return {HTMLDOMElement} - The container holds rendererd canvas
	 */
  PANOLENS.Viewer.prototype.getContainer = function () {
    return this.container
  }

  /**
	 * Get control name
	 * @return {string} - Control name. 'orbit' or 'device-orientation'
	 */
  PANOLENS.Viewer.prototype.getControlName = function () {
    return this.control.name
  }

  /**
	 * Get next navigation control name
	 * @return {string} - Next control name
	 */
  PANOLENS.Viewer.prototype.getNextControlName = function () {
    return this.controls[ this.getNextControlIndex() ].name
  }

  /**
	 * Get next navigation control index
	 * @return {number} - Next control index
	 */
  PANOLENS.Viewer.prototype.getNextControlIndex = function () {
    var controls, control, nextIndex

    controls = this.controls
    control = this.control
    nextIndex = controls.indexOf(control) + 1

    return (nextIndex >= controls.length) ? 0 : nextIndex
  }

  /**
	 * Set field of view of camera
	 */
  PANOLENS.Viewer.prototype.setCameraFov = function (fov) {
    this.camera.fov = fov
    this.camera.updateProjectionMatrix()
  }

  /**
	 * Enable control by index
	 * @param  {PANOLENS.Controls} index - Index of camera control
	 */
  PANOLENS.Viewer.prototype.enableControl = function (index) {
    index = (index >= 0 && index < this.controls.length) ? index : 0

    this.control.enabled = false

    this.control = this.controls[ index ]

    this.control.enabled = true

    switch (index) {
      case PANOLENS.Controls.ORBIT:

        this.camera.position.copy(this.panorama.position)
        this.camera.position.z += 1

        break

      case PANOLENS.Controls.DEVICEORIENTATION:

        this.camera.position.copy(this.panorama.position)

        break

      default:

        break
    }

    this.control.update()

    this.activateWidgetItem(index, undefined)
  }

  /**
	 * Disable current control
	 */
  PANOLENS.Viewer.prototype.disableControl = function () {
    this.control.enabled = false
  }

  /**
	 * Toggle next control
	 */
  PANOLENS.Viewer.prototype.toggleNextControl = function () {
    this.enableControl(this.getNextControlIndex())
  }

  /**
	 * Screen Space Projection
	 */
  PANOLENS.Viewer.prototype.getScreenVector = function (worldVector) {
    var vector = worldVector.clone()
    var widthHalf = (this.container.clientWidth) / 2
    var heightHalf = this.container.clientHeight / 2

    vector.project(this.camera)

    vector.x = (vector.x * widthHalf) + widthHalf
    vector.y = -(vector.y * heightHalf) + heightHalf
    vector.z = 0

    return vector
  }

  /**
	 * Check Sprite in Viewport
	 */
  PANOLENS.Viewer.prototype.checkSpriteInViewport = function (sprite) {
    this.camera.matrixWorldInverse.getInverse(this.camera.matrixWorld)
    this.cameraViewProjectionMatrix.multiplyMatrices(this.camera.projectionMatrix, this.camera.matrixWorldInverse)
    this.cameraFrustum.setFromMatrix(this.cameraViewProjectionMatrix)

    return sprite.visible && this.cameraFrustum.intersectsSprite(sprite)
  }

  /**
	 * Reverse dragging direction
	 */
  PANOLENS.Viewer.prototype.reverseDraggingDirection = function () {
    this.OrbitControls.rotateSpeed *= -1
    this.OrbitControls.momentumScalingFactor *= -1
  }

  /**
	 * Add reticle
	 */
  PANOLENS.Viewer.prototype.addReticle = function () {
    this.reticle = new PANOLENS.Reticle(0xffffff,
      this.options.autoReticleSelect,
      PANOLENS.DataImage.ReticleIdle,
      PANOLENS.DataImage.ReticleDwell,
      this.options.dwellTime,
      45)
    this.reticle.position.z = -10
    this.camera.add(this.reticle)
    this.scene.add(this.camera)
  }

  /**
	 * Tween control looking center
	 * @param {THREE.Vector3} vector - Vector to be looked at the center
	 * @param {number} [duration=1000] - Duration to tween
	 * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
	 */
  PANOLENS.Viewer.prototype.tweenControlCenter = function (vector, duration, easing) {
    if (this.control !== this.OrbitControls) {
      return
    }

    // Pass in arguments as array
    if (vector instanceof Array) {
      duration = vector[ 1 ]
      easing = vector[ 2 ]
      vector = vector[ 0 ]
    }

    duration = duration !== undefined ? duration : 1000
    easing = easing || TWEEN.Easing.Exponential.Out

    var scope, ha, va, chv, cvv, hv, vv, vptc, ov, nv

    scope = this

    chv = this.camera.getWorldDirection()
    cvv = chv.clone()

    vptc = this.panorama.getWorldPosition().sub(this.camera.getWorldPosition())

    hv = vector.clone()
    // Scale effect
    hv.x *= -1
    hv.add(vptc).normalize()
    vv = hv.clone()

    chv.y = 0
    hv.y = 0

    ha = Math.atan2(hv.z, hv.x) - Math.atan2(chv.z, chv.x)
    ha = ha > Math.PI ? ha - 2 * Math.PI : ha
    ha = ha < -Math.PI ? ha + 2 * Math.PI : ha
    va = Math.abs(cvv.angleTo(chv) + (cvv.y * vv.y <= 0 ? vv.angleTo(hv) : -vv.angleTo(hv)))
    va *= vv.y < cvv.y ? 1 : -1

    ov = { left: 0, up: 0 }
    nv = { left: 0, up: 0 }

    this.tweenLeftAnimation.stop()
    this.tweenUpAnimation.stop()

    this.tweenLeftAnimation = new TWEEN.Tween(ov)
      .to({ left: ha }, duration)
      .easing(easing)
      .onUpdate(function () {
        scope.control.rotateLeft(this.left - nv.left)
        nv.left = this.left
      })
      .start()

    this.tweenUpAnimation = new TWEEN.Tween(ov)
      .to({ up: va }, duration)
      .easing(easing)
      .onUpdate(function () {
        scope.control.rotateUp(this.up - nv.up)
        nv.up = this.up
      })
      .start()
  }

  /**
	 * Tween control looking center by object
	 * @param {THREE.Object3D} object - Object to be looked at the center
	 * @param {number} [duration=1000] - Duration to tween
	 * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
	 */
  PANOLENS.Viewer.prototype.tweenControlCenterByObject = function (object, duration, easing) {
    var isUnderScalePlaceHolder = false

    object.traverseAncestors(function (ancestor) {
      if (ancestor.scalePlaceHolder) {
        isUnderScalePlaceHolder = true
      }
    })

    if (isUnderScalePlaceHolder) {
      var invertXVector = new THREE.Vector3(-1, 1, 1)

      this.tweenControlCenter(object.getWorldPosition().multiply(invertXVector), duration, easing)
    } else {
      this.tweenControlCenter(object.getWorldPosition(), duration, easing)
    }
  }

  /**
	 * This is called when window size is changed
	 * @fires PANOLENS.Viewer#window-resize
	 * @param {number} [windowWidth] - Specify if custom element has changed width
	 * @param {number} [windowHeight] - Specify if custom element has changed height
	 */
  PANOLENS.Viewer.prototype.onWindowResize = function (windowWidth, windowHeight) {
    var width, height, expand

    expand = this.container.classList.contains('panolens-container') || this.container.isFullscreen

    if (windowWidth !== undefined && windowHeight !== undefined) {
      width = windowWidth
      height = windowHeight
      this.container._width = windowWidth
      this.container._height = windowHeight
    } else {
      width = expand ? Math.max(document.documentElement.clientWidth, window.innerWidth || 0) : this.container.clientWidth
      height = expand ? Math.max(document.documentElement.clientHeight, window.innerHeight || 0) : this.container.clientHeight
      this.container._width = width
      this.container._height = height
    }

    this.camera.aspect = width / height
    this.camera.updateProjectionMatrix()

    this.renderer.setSize(width, height)

    // Update reticle
    if (this.options.enableReticle || this.tempEnableReticle) {
      this.updateReticleEvent()
    }

    /**
		 * Window resizing event
		 * @type {object}
		 * @event PANOLENS.Viewer#window-resize
		 * @property {number} width  - Width of the window
		 * @property {number} height - Height of the window
		 */
    this.dispatchEvent({ type: 'window-resize', width: width, height: height })
    this.scene.traverse(function (object) {
      if (object.dispatchEvent) {
        object.dispatchEvent({ type: 'window-resize', width: width, height: height })
      }
    })
  }

  PANOLENS.Viewer.prototype.addOutputElement = function () {
    var element = document.createElement('div')
    element.style.position = 'absolute'
    element.style.right = '10px'
    element.style.top = '10px'
    element.style.color = '#fff'
    this.container.appendChild(element)
    this.outputDivElement = element
  }

  /**
	 * Output infospot attach position in developer console by holding down Ctrl button
	 */
  PANOLENS.Viewer.prototype.outputInfospotPosition = function () {
    var intersects, point, panoramaWorldPosition, outputPosition

    intersects = this.raycaster.intersectObject(this.panorama, true)

    if (intersects.length > 0) {
      point = intersects[0].point
      panoramaWorldPosition = this.panorama.getWorldPosition()

      // Panorama is scaled -1 on X axis
      outputPosition = new THREE.Vector3(
        -(point.x - panoramaWorldPosition.x).toFixed(2),
        (point.y - panoramaWorldPosition.y).toFixed(2),
        (point.z - panoramaWorldPosition.z).toFixed(2)
      )

      switch (this.options.output) {
        case 'console':
          console.info(outputPosition.x + ', ' + outputPosition.y + ', ' + outputPosition.z)
          break

        case 'overlay':
          this.outputDivElement.textContent = outputPosition.x + ', ' + outputPosition.y + ', ' + outputPosition.z
          break

        default:
          break
      }
    }
  }

  PANOLENS.Viewer.prototype.onMouseDown = function (event) {
    event.preventDefault()

    this.userMouse.x = (event.clientX >= 0) ? event.clientX : event.touches[0].clientX
    this.userMouse.y = (event.clientY >= 0) ? event.clientY : event.touches[0].clientY
    this.userMouse.type = 'mousedown'
    this.onTap(event)
  }

  PANOLENS.Viewer.prototype.onMouseMove = function (event) {
    event.preventDefault()
    this.userMouse.type = 'mousemove'
    this.onTap(event)
  }

  PANOLENS.Viewer.prototype.onMouseUp = function (event) {
    var onTarget = false, type

    this.userMouse.type = 'mouseup'

    type = (this.userMouse.x >= event.clientX - this.options.clickTolerance &&
				this.userMouse.x <= event.clientX + this.options.clickTolerance &&
				this.userMouse.y >= event.clientY - this.options.clickTolerance &&
				this.userMouse.y <= event.clientY + this.options.clickTolerance) ||
				(event.changedTouches &&
				this.userMouse.x >= event.changedTouches[0].clientX - this.options.clickTolerance &&
				this.userMouse.x <= event.changedTouches[0].clientX + this.options.clickTolerance &&
				this.userMouse.y >= event.changedTouches[0].clientY - this.options.clickTolerance &&
				this.userMouse.y <= event.changedTouches[0].clientY + this.options.clickTolerance)
      ? 'click' : undefined

    // Event should happen on canvas
    if (event && event.target && !event.target.classList.contains('panolens-canvas')) { return }

    event.preventDefault()

    if (event.changedTouches && event.changedTouches.length === 1) {
      onTarget = this.onTap({ clientX: event.changedTouches[0].clientX, clientY: event.changedTouches[0].clientY }, type)
    } else {
      onTarget = this.onTap(event, type)
    }

    this.userMouse.type = 'none'

    if (onTarget) {
      return
    }

    if (type === 'click') {
      this.options.autoHideInfospot && this.panorama && this.panorama.toggleInfospotVisibility()
      this.options.autoHideControlBar && this.toggleControlBar()
    }
  }

  PANOLENS.Viewer.prototype.onTap = function (event, type) {
    var intersects, intersect_entity, intersect

    this.raycasterPoint.x = ((event.clientX - this.container.offsetLeft) / this.container.clientWidth) * 2 - 1
    this.raycasterPoint.y = -((event.clientY - this.container.offsetTop) / this.container.clientHeight) * 2 + 1

    this.raycaster.setFromCamera(this.raycasterPoint, this.camera)

    // Return if no panorama
    if (!this.panorama) {
      return
    }

    // output infospot information
    if (event.type !== 'mousedown' && PANOLENS.Utils.checkTouchSupported() || this.OUTPUT_INFOSPOT) {
      this.outputInfospotPosition()
    }

    intersects = this.raycaster.intersectObjects(this.panorama.children, true)

    intersect_entity = this.getConvertedIntersect(intersects)

    intersect = (intersects.length > 0) ? intersects[0].object : intersect

    if (this.userMouse.type === 'mouseup') {
      if (intersect_entity && this.pressEntityObject === intersect_entity && this.pressEntityObject.dispatchEvent) {
        this.pressEntityObject.dispatchEvent({ type: 'pressstop-entity', mouseEvent: event })
      }

      this.pressEntityObject = undefined
    }

    if (this.userMouse.type === 'mouseup') {
      if (intersect && this.pressObject === intersect && this.pressObject.dispatchEvent) {
        this.pressObject.dispatchEvent({ type: 'pressstop', mouseEvent: event })
      }

      this.pressObject = undefined
    }

    if (type === 'click') {
      this.panorama.dispatchEvent({ type: 'click', intersects: intersects, mouseEvent: event })

      if (intersect_entity && intersect_entity.dispatchEvent) {
        intersect_entity.dispatchEvent({ type: 'click-entity', mouseEvent: event })
      }

      if (intersect && intersect.dispatchEvent) {
        intersect.dispatchEvent({ type: 'click', mouseEvent: event })
      }
    } else {
      this.panorama.dispatchEvent({ type: 'hover', intersects: intersects, mouseEvent: event })

      if ((this.hoverObject && intersects.length > 0 && this.hoverObject !== intersect_entity) ||
				(this.hoverObject && intersects.length === 0)) {
        if (this.hoverObject.dispatchEvent) {
          this.hoverObject.dispatchEvent({ type: 'hoverleave', mouseEvent: event })

          // Cancel dwelling
          this.reticle.cancelDwelling()
        }

        this.hoverObject = undefined
      }

      if (intersect_entity && intersects.length > 0) {
        if (this.hoverObject !== intersect_entity) {
          this.hoverObject = intersect_entity

          if (this.hoverObject.dispatchEvent) {
            this.hoverObject.dispatchEvent({ type: 'hoverenter', mouseEvent: event })

            // Start reticle timer
            if (this.options.autoReticleSelect && this.options.enableReticle || this.tempEnableReticle) {
              this.reticle.startDwelling(this.onTap.bind(this, event, 'click'))
            }
          }
        }

        if (this.userMouse.type === 'mousedown' && this.pressEntityObject != intersect_entity) {
          this.pressEntityObject = intersect_entity

          if (this.pressEntityObject.dispatchEvent) {
            this.pressEntityObject.dispatchEvent({ type: 'pressstart-entity', mouseEvent: event })
          }
        }

        if (this.userMouse.type === 'mousedown' && this.pressObject != intersect) {
          this.pressObject = intersect

          if (this.pressObject.dispatchEvent) {
            this.pressObject.dispatchEvent({ type: 'pressstart', mouseEvent: event })
          }
        }

        if (this.userMouse.type === 'mousemove' || this.options.enableReticle) {
          if (intersect && intersect.dispatchEvent) {
            intersect.dispatchEvent({ type: 'hover', mouseEvent: event })
          }

          if (this.pressEntityObject && this.pressEntityObject.dispatchEvent) {
            this.pressEntityObject.dispatchEvent({ type: 'pressmove-entity', mouseEvent: event })
          }

          if (this.pressObject && this.pressObject.dispatchEvent) {
            this.pressObject.dispatchEvent({ type: 'pressmove', mouseEvent: event })
          }
        }
      }

      if (!intersect_entity && this.pressEntityObject && this.pressEntityObject.dispatchEvent) {
        this.pressEntityObject.dispatchEvent({ type: 'pressstop-entity', mouseEvent: event })

        this.pressEntityObject = undefined
      }

      if (!intersect && this.pressObject && this.pressObject.dispatchEvent) {
        this.pressObject.dispatchEvent({ type: 'pressstop', mouseEvent: event })

        this.pressObject = undefined
      }
    }

    // Infospot handler
    if (intersect && intersect instanceof PANOLENS.Infospot) {
      this.infospot = intersect

      if (type === 'click') {
        return true
      }
    } else if (this.infospot) {
      this.hideInfospot()
    }
  }

  PANOLENS.Viewer.prototype.getConvertedIntersect = function (intersects) {
    var intersect

    for (var i = 0; i < intersects.length; i++) {
      if (intersects[i].distance >= 0 && intersects[i].object && !intersects[i].object.passThrough) {
        if (intersects[i].object.entity && intersects[i].object.entity.passThrough) {
          continue
        } else if (intersects[i].object.entity && !intersects[i].object.entity.passThrough) {
          intersect = intersects[i].object.entity
          break
        } else {
          intersect = intersects[i].object
          break
        }
      }
    }

    return intersect
  }

  PANOLENS.Viewer.prototype.hideInfospot = function (intersects) {
    if (this.infospot) {
      this.infospot.onHoverEnd()

      this.infospot = undefined
    }
  }

  /**
	 * Toggle control bar
	 * @fires [PANOLENS.Viewer#control-bar-toggle]
	 */
  PANOLENS.Viewer.prototype.toggleControlBar = function () {
    /**
		 * Toggle control bar event
		 * @type {object}
		 * @event PANOLENS.Viewer#control-bar-toggle
		 */
    this.widget && this.widget.dispatchEvent({ type: 'control-bar-toggle' })
  }

  PANOLENS.Viewer.prototype.onKeyDown = function (event) {
    if (this.options.output && this.options.output !== 'none' && event.key === 'Control') {
      this.OUTPUT_INFOSPOT = true
    }
  }

  PANOLENS.Viewer.prototype.onKeyUp = function (event) {
    this.OUTPUT_INFOSPOT = false
  }

  /**
	 * Update control and callbacks
	 */
  PANOLENS.Viewer.prototype.update = function () {
    TWEEN.update()

    this.updateCallbacks.forEach(function (callback) { callback() })

    this.control.update()

    this.scene.traverse(function (child) {
      if (child instanceof PANOLENS.Infospot &&
				child.element &&
				(this.hoverObject === child ||
					child.element.style.display !== 'none' ||
					(child.element.left && child.element.left.style.display !== 'none') ||
					(child.element.right && child.element.right.style.display !== 'none'))) {
        if (this.checkSpriteInViewport(child)) {
          var vector = this.getScreenVector(child.getWorldPosition())
          child.translateElement(vector.x, vector.y)
        } else {
          child.onDismiss()
        }
      }
    }.bind(this))
  }

  /**
	 * Rendering function to be called on every animation frame
	 */
  PANOLENS.Viewer.prototype.render = function () {
    if (this.mode === PANOLENS.Modes.CARDBOARD || this.mode === PANOLENS.Modes.STEREO) {
      this.effect.render(this.scene, this.camera)
    } else {
      this.renderer.render(this.scene, this.camera)
    }
  }

  PANOLENS.Viewer.prototype.animate = function () {
    this.requestAnimationId = window.requestAnimationFrame(this.animate.bind(this))

    this.onChange()
  }

  PANOLENS.Viewer.prototype.onChange = function () {
    this.update()
    this.render()
  }

  /**
	 * Register mouse and touch event on container
	 */
  PANOLENS.Viewer.prototype.registerMouseAndTouchEvents = function () {
    this.container.addEventListener('mousedown', 	this.HANDLER_MOUSE_DOWN, false)
    this.container.addEventListener('mousemove', 	this.HANDLER_MOUSE_MOVE, false)
    this.container.addEventListener('mouseup', 	this.HANDLER_MOUSE_UP, false)
    this.container.addEventListener('touchstart', 	this.HANDLER_MOUSE_DOWN, false)
    this.container.addEventListener('touchend', 	this.HANDLER_MOUSE_UP, false)
  }

  /**
	 * Unregister mouse and touch event on container
	 */
  PANOLENS.Viewer.prototype.unregisterMouseAndTouchEvents = function () {
    this.container.removeEventListener('mousedown', this.HANDLER_MOUSE_DOWN, false)
    this.container.removeEventListener('mousemove', this.HANDLER_MOUSE_MOVE, false)
    this.container.removeEventListener('mouseup', this.HANDLER_MOUSE_UP, false)
    this.container.removeEventListener('touchstart', this.HANDLER_MOUSE_DOWN, false)
    this.container.removeEventListener('touchend', this.HANDLER_MOUSE_UP, false)
  }

  /**
	 * Register reticle event
	 */
  PANOLENS.Viewer.prototype.registerReticleEvent = function () {
    this.addUpdateCallback(this.HANDLER_TAP)
  }

  /**
	 * Unregister reticle event
	 */
  PANOLENS.Viewer.prototype.unregisterReticleEvent = function () {
    this.removeUpdateCallback(this.HANDLER_TAP)
  }

  /**
	 * Update reticle event
	 */
  PANOLENS.Viewer.prototype.updateReticleEvent = function () {
    var centerX, centerY

    centerX = this.container.clientWidth / 2 + this.container.offsetLeft
    centerY = this.container.clientHeight / 2

    this.removeUpdateCallback(this.HANDLER_TAP)
    this.HANDLER_TAP = this.onTap.bind(this, { clientX: centerX, clientY: centerY })
    this.addUpdateCallback(this.HANDLER_TAP)
  }

  /**
	 * Register container and window listeners
	 */
  PANOLENS.Viewer.prototype.registerEventListeners = function () {
    // Resize Event
    window.addEventListener('resize', this.HANDLER_WINDOW_RESIZE, true)

    // Keyboard Event
    window.addEventListener('keydown', this.HANDLER_KEY_DOWN, true)
    window.addEventListener('keyup', this.HANDLER_KEY_UP, true)
  }

  /**
	 * Unregister container and window listeners
	 */
  PANOLENS.Viewer.prototype.unregisterEventListeners = function () {
    // Resize Event
    window.removeEventListener('resize', this.HANDLER_WINDOW_RESIZE, true)

    // Keyboard Event
    window.removeEventListener('keydown', this.HANDLER_KEY_DOWN, true)
    window.removeEventListener('keyup', this.HANDLER_KEY_UP, true)
  }

  /**
	 * Dispose all scene objects and clear cache
	 */
  PANOLENS.Viewer.prototype.dispose = function () {
    // Unregister dom event listeners
    this.unregisterEventListeners()

    // recursive disposal on 3d objects
    function recursiveDispose (object) {
      for (var i = object.children.length - 1; i >= 0; i--) {
        recursiveDispose(object.children[i])
        object.remove(object.children[i])
      }

      if (object instanceof PANOLENS.Infospot) {
        object.dispose()
      }

      object.geometry && object.geometry.dispose()
      object.material && object.material.dispose()
    }

    recursiveDispose(this.scene)

    // dispose widget
    if (this.widget) {
      this.widget.dispose()
      this.widget = null
    }

    // clear cache
    if (THREE.Cache && THREE.Cache.enabled) {
      THREE.Cache.clear()
    }
  }

  /**
	 * Destory viewer by disposing and stopping requestAnimationFrame
	 */
  PANOLENS.Viewer.prototype.destory = function () {
    this.dispose()
    this.render()
    window.cancelAnimationFrame(this.requestAnimationId)
  }

  /**
	 * On panorama dispose
	 */
  PANOLENS.Viewer.prototype.onPanoramaDispose = function (panorama) {
    if (panorama instanceof PANOLENS.VideoPanorama) {
      this.hideVideoWidget()
    }

    if (panorama === this.panorama) {
      this.panorama = null
    }
  }

  /**
	 * Load ajax call
	 * @param {string} url - URL to be requested
	 * @param {function} [callback] - Callback after request completes
	 */
  PANOLENS.Viewer.prototype.loadAsyncRequest = function (url, callback) {
    var request = new XMLHttpRequest()
    request.onloadend = function (event) {
      callback && callback(event)
    }
    request.open('GET', url, true)
    request.send(null)
  }

  /**
	 * View indicator in upper left
	 * */
  PANOLENS.Viewer.prototype.addViewIndicator = function () {
    var scope = this

    function loadViewIndicator (asyncEvent) {
      if (asyncEvent.loaded === 0) return

      var viewIndicatorDiv = asyncEvent.target.responseXML.documentElement
      viewIndicatorDiv.style.width = scope.viewIndicatorSize + 'px'
      viewIndicatorDiv.style.height = scope.viewIndicatorSize + 'px'
      viewIndicatorDiv.style.position = 'absolute'
      viewIndicatorDiv.style.top = '10px'
      viewIndicatorDiv.style.left = '10px'
      viewIndicatorDiv.style.opacity = '0.5'
      viewIndicatorDiv.style.cursor = 'pointer'
      viewIndicatorDiv.id = 'panolens-view-indicator-container'

      scope.container.appendChild(viewIndicatorDiv)

      var indicator = viewIndicatorDiv.querySelector('#indicator')
      var setIndicatorD = function () {
        scope.radius = scope.viewIndicatorSize * 0.225
        scope.currentPanoAngle = scope.camera.rotation.y - THREE.Math.degToRad(90)
        scope.fovAngle = THREE.Math.degToRad(scope.camera.fov)
        scope.leftAngle = -scope.currentPanoAngle - scope.fovAngle / 2
        scope.rightAngle = -scope.currentPanoAngle + scope.fovAngle / 2
        scope.leftX = scope.radius * Math.cos(scope.leftAngle)
        scope.leftY = scope.radius * Math.sin(scope.leftAngle)
        scope.rightX = scope.radius * Math.cos(scope.rightAngle)
        scope.rightY = scope.radius * Math.sin(scope.rightAngle)
        scope.indicatorD = 'M ' + scope.leftX + ' ' + scope.leftY + ' A ' + scope.radius + ' ' + scope.radius + ' 0 0 1 ' + scope.rightX + ' ' + scope.rightY

        if (scope.leftX && scope.leftY && scope.rightX && scope.rightY && scope.radius) {
          indicator.setAttribute('d', scope.indicatorD)
        }
      }

      scope.addUpdateCallback(setIndicatorD)

      var indicatorOnMouseEnter = function () {
        this.style.opacity = '1'
      }

      var indicatorOnMouseLeave = function () {
        this.style.opacity = '0.5'
      }

      viewIndicatorDiv.addEventListener('mouseenter', indicatorOnMouseEnter)
      viewIndicatorDiv.addEventListener('mouseleave', indicatorOnMouseLeave)
    }

    this.loadAsyncRequest(PANOLENS.DataImage.ViewIndicator, loadViewIndicator)
  }

  /**
	 * Append custom control item to existing control bar
	 * @param {object} [option={}] - Style object to overwirte default element style. It takes 'style', 'onTap' and 'group' properties.
	 */
  PANOLENS.Viewer.prototype.appendControlItem = function (option) {
    var item = this.widget.createCustomItem(option)

    if (option.group === 'video') {
      this.widget.videoElement.appendChild(item)
    } else {
      this.widget.barElement.appendChild(item)
    }

    return item
  }
})()
;(function e (t, n, r) { function s (o, u) { if (!n[o]) { if (!t[o]) { var a = typeof require === 'function' && require; if (!u && a) return a(o, !0); if (i) return i(o, !0); var f = new Error("Cannot find module '" + o + "'"); throw f.code = 'MODULE_NOT_FOUND', f } var l = n[o] = {exports: {}}; t[o][0].call(l.exports, function (e) { var n = t[o][1][e]; return s(n || e) }, l, l.exports, e, t, n, r) } return n[o].exports } var i = typeof require === 'function' && require; for (var o = 0; o < r.length; o++)s(r[o]); return s })({1: [function (require, module, exports) {
  var createLayout = require('layout-bmfont-text')
  var inherits = require('inherits')
  var createIndices = require('quad-indices')
  var buffer = require('three-buffer-vertex-data')
  var assign = require('object-assign')

  var vertices = require('./lib/vertices')
  var utils = require('./lib/utils')

  var Base = THREE.BufferGeometry

  module.exports = function createTextGeometry (opt) {
	  return new TextGeometry(opt)
  }

  function TextGeometry (opt) {
	  Base.call(this)

	  if (typeof opt === 'string') {
	    opt = { text: opt }
	  }

	  // use these as default values for any subsequent
	  // calls to update()
	  this._opt = assign({}, opt)

	  // also do an initial setup...
	  if (opt) this.update(opt)
  }

  inherits(TextGeometry, Base)

  TextGeometry.prototype.update = function (opt) {
	  if (typeof opt === 'string') {
	    opt = { text: opt }
	  }

	  // use constructor defaults
	  opt = assign({}, this._opt, opt)

	  if (!opt.font) {
	    throw new TypeError('must specify a { font } in options')
	  }

	  this.layout = createLayout(opt)

	  // get vec2 texcoords
	  var flipY = opt.flipY !== false

	  // the desired BMFont data
	  var font = opt.font

	  // determine texture size from font file
	  var texWidth = font.common.scaleW
	  var texHeight = font.common.scaleH

	  // get visible glyphs
	  var glyphs = this.layout.glyphs.filter(function (glyph) {
	    var bitmap = glyph.data
	    return bitmap.width * bitmap.height > 0
	  })

	  // provide visible glyphs for convenience
	  this.visibleGlyphs = glyphs

	  // get common vertex data
	  var positions = vertices.positions(glyphs)
	  var uvs = vertices.uvs(glyphs, texWidth, texHeight, flipY)
	  var indices = createIndices({
	    clockwise: true,
	    type: 'uint16',
	    count: glyphs.length
	  })

	  // update vertex data
	  buffer.index(this, indices, 1, 'uint16')
	  buffer.attr(this, 'position', positions, 2)
	  buffer.attr(this, 'uv', uvs, 2)

	  // update multipage data
	  if (!opt.multipage && 'page' in this.attributes) {
	    // disable multipage rendering
	    this.removeAttribute('page')
	  } else if (opt.multipage) {
	    var pages = vertices.pages(glyphs)
	    // enable multipage rendering
	    buffer.attr(this, 'page', pages, 1)
	  }
  }

  TextGeometry.prototype.computeBoundingSphere = function () {
	  if (this.boundingSphere === null) {
	    this.boundingSphere = new THREE.Sphere()
	  }

	  var positions = this.attributes.position.array
	  var itemSize = this.attributes.position.itemSize
	  if (!positions || !itemSize || positions.length < 2) {
	    this.boundingSphere.radius = 0
	    this.boundingSphere.center.set(0, 0, 0)
	    return
	  }
	  utils.computeSphere(positions, this.boundingSphere)
	  if (isNaN(this.boundingSphere.radius)) {
	    console.error('THREE.BufferGeometry.computeBoundingSphere(): ' +
	      'Computed radius is NaN. The ' +
	      '"position" attribute is likely to have NaN values.')
	  }
  }

  TextGeometry.prototype.computeBoundingBox = function () {
	  if (this.boundingBox === null) {
	    this.boundingBox = new THREE.Box3()
	  }

	  var bbox = this.boundingBox
	  var positions = this.attributes.position.array
	  var itemSize = this.attributes.position.itemSize
	  if (!positions || !itemSize || positions.length < 2) {
	    bbox.makeEmpty()
	    return
	  }
	  utils.computeBox(positions, bbox)
  }
}, {'./lib/utils': 2, './lib/vertices': 3, 'inherits': 4, 'layout-bmfont-text': 5, 'object-assign': 26, 'quad-indices': 27, 'three-buffer-vertex-data': 31}],
2: [function (require, module, exports) {
  var itemSize = 2
  var box = { min: [0, 0], max: [0, 0] }

  function bounds (positions) {
	  var count = positions.length / itemSize
	  box.min[0] = positions[0]
	  box.min[1] = positions[1]
	  box.max[0] = positions[0]
	  box.max[1] = positions[1]

	  for (var i = 0; i < count; i++) {
	    var x = positions[i * itemSize + 0]
	    var y = positions[i * itemSize + 1]
	    box.min[0] = Math.min(x, box.min[0])
	    box.min[1] = Math.min(y, box.min[1])
	    box.max[0] = Math.max(x, box.max[0])
	    box.max[1] = Math.max(y, box.max[1])
	  }
  }

  module.exports.computeBox = function (positions, output) {
	  bounds(positions)
	  output.min.set(box.min[0], box.min[1], 0)
	  output.max.set(box.max[0], box.max[1], 0)
  }

  module.exports.computeSphere = function (positions, output) {
	  bounds(positions)
	  var minX = box.min[0]
	  var minY = box.min[1]
	  var maxX = box.max[0]
	  var maxY = box.max[1]
	  var width = maxX - minX
	  var height = maxY - minY
	  var length = Math.sqrt(width * width + height * height)
	  output.center.set(minX + width / 2, minY + height / 2, 0)
	  output.radius = length / 2
  }
}, {}],
3: [function (require, module, exports) {
  module.exports.pages = function pages (glyphs) {
	  var pages = new Float32Array(glyphs.length * 4 * 1)
	  var i = 0
	  glyphs.forEach(function (glyph) {
	    var id = glyph.data.page || 0
	    pages[i++] = id
	    pages[i++] = id
	    pages[i++] = id
	    pages[i++] = id
	  })
	  return pages
  }

  module.exports.uvs = function uvs (glyphs, texWidth, texHeight, flipY) {
	  var uvs = new Float32Array(glyphs.length * 4 * 2)
	  var i = 0
	  glyphs.forEach(function (glyph) {
	    var bitmap = glyph.data
	    var bw = (bitmap.x + bitmap.width)
	    var bh = (bitmap.y + bitmap.height)

	    // top left position
	    var u0 = bitmap.x / texWidth
	    var v1 = bitmap.y / texHeight
	    var u1 = bw / texWidth
	    var v0 = bh / texHeight

	    if (flipY) {
	      v1 = (texHeight - bitmap.y) / texHeight
	      v0 = (texHeight - bh) / texHeight
	    }

	    // BL
	    uvs[i++] = u0
	    uvs[i++] = v1
	    // TL
	    uvs[i++] = u0
	    uvs[i++] = v0
	    // TR
	    uvs[i++] = u1
	    uvs[i++] = v0
	    // BR
	    uvs[i++] = u1
	    uvs[i++] = v1
	  })
	  return uvs
  }

  module.exports.positions = function positions (glyphs) {
	  var positions = new Float32Array(glyphs.length * 4 * 2)
	  var i = 0
	  glyphs.forEach(function (glyph) {
	    var bitmap = glyph.data

	    // bottom left position
	    var x = glyph.position[0] + bitmap.xoffset
	    var y = glyph.position[1] + bitmap.yoffset

	    // quad size
	    var w = bitmap.width
	    var h = bitmap.height

	    // BL
	    positions[i++] = x
	    positions[i++] = y
	    // TL
	    positions[i++] = x
	    positions[i++] = y + h
	    // TR
	    positions[i++] = x + w
	    positions[i++] = y + h
	    // BR
	    positions[i++] = x + w
	    positions[i++] = y
	  })
	  return positions
  }
}, {}],
4: [function (require, module, exports) {
  if (typeof Object.create === 'function') {
	  // implementation from standard node.js 'util' module
	  module.exports = function inherits (ctor, superCtor) {
	    ctor.super_ = superCtor
	    ctor.prototype = Object.create(superCtor.prototype, {
	      constructor: {
	        value: ctor,
	        enumerable: false,
	        writable: true,
	        configurable: true
	      }
	    })
	  }
  } else {
	  // old school shim for old browsers
	  module.exports = function inherits (ctor, superCtor) {
	    ctor.super_ = superCtor
	    var TempCtor = function () {}
	    TempCtor.prototype = superCtor.prototype
	    ctor.prototype = new TempCtor()
	    ctor.prototype.constructor = ctor
	  }
  }
}, {}],
5: [function (require, module, exports) {
  var wordWrap = require('word-wrapper')
  var xtend = require('xtend')
  var findChar = require('indexof-property')('id')
  var number = require('as-number')

  var X_HEIGHTS = ['x', 'e', 'a', 'o', 'n', 's', 'r', 'c', 'u', 'm', 'v', 'w', 'z']
  var M_WIDTHS = ['m', 'w']
  var CAP_HEIGHTS = ['H', 'I', 'N', 'E', 'F', 'K', 'L', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

  var TAB_ID = '\t'.charCodeAt(0)
  var SPACE_ID = ' '.charCodeAt(0)
  var ALIGN_LEFT = 0,
	    ALIGN_CENTER = 1,
	    ALIGN_RIGHT = 2

  module.exports = function createLayout (opt) {
	  return new TextLayout(opt)
  }

  function TextLayout (opt) {
	  this.glyphs = []
	  this._measure = this.computeMetrics.bind(this)
	  this.update(opt)
  }

  TextLayout.prototype.update = function (opt) {
	  opt = xtend({
	    measure: this._measure
	  }, opt)
	  this._opt = opt
	  this._opt.tabSize = number(this._opt.tabSize, 4)

	  if (!opt.font) { throw new Error('must provide a valid bitmap font') }

	  var glyphs = this.glyphs
	  var text = opt.text || ''
	  var font = opt.font
	  this._setupSpaceGlyphs(font)

	  var lines = wordWrap.lines(text, opt)
	  var minWidth = opt.width || 0

	  // clear glyphs
	  glyphs.length = 0

	  // get max line width
	  var maxLineWidth = lines.reduce(function (prev, line) {
	    return Math.max(prev, line.width, minWidth)
	  }, 0)

	  // the pen position
	  var x = 0
	  var y = 0
	  var lineHeight = number(opt.lineHeight, font.common.lineHeight)
	  var baseline = font.common.base
	  var descender = lineHeight - baseline
	  var letterSpacing = opt.letterSpacing || 0
	  var height = lineHeight * lines.length - descender
	  var align = getAlignType(this._opt.align)

	  // draw text along baseline
	  y -= height

	  // the metrics for this text layout
	  this._width = maxLineWidth
	  this._height = height
	  this._descender = lineHeight - baseline
	  this._baseline = baseline
	  this._xHeight = getXHeight(font)
	  this._capHeight = getCapHeight(font)
	  this._lineHeight = lineHeight
	  this._ascender = lineHeight - descender - this._xHeight

	  // layout each glyph
	  var self = this
	  lines.forEach(function (line, lineIndex) {
	    var start = line.start
	    var end = line.end
	    var lineWidth = line.width
	    var lastGlyph

	    // for each glyph in that line...
	    for (var i = start; i < end; i++) {
	      var id = text.charCodeAt(i)
	      var glyph = self.getGlyph(font, id)
	      if (glyph) {
	        if (lastGlyph) { x += getKerning(font, lastGlyph.id, glyph.id) }

	        var tx = x
	        if (align === ALIGN_CENTER) { tx += (maxLineWidth - lineWidth) / 2 } else if (align === ALIGN_RIGHT) { tx += (maxLineWidth - lineWidth) }

	        glyphs.push({
	          position: [tx, y],
	          data: glyph,
	          index: i,
	          line: lineIndex
	        })

	        // move pen forward
	        x += glyph.xadvance + letterSpacing
	        lastGlyph = glyph
	      }
	    }

	    // next line down
	    y += lineHeight
	    x = 0
	  })
	  this._linesTotal = lines.length
  }

  TextLayout.prototype._setupSpaceGlyphs = function (font) {
	  // These are fallbacks, when the font doesn't include
	  // ' ' or '\t' glyphs
	  this._fallbackSpaceGlyph = null
	  this._fallbackTabGlyph = null

	  if (!font.chars || font.chars.length === 0) { return }

	  // try to get space glyph
	  // then fall back to the 'm' or 'w' glyphs
	  // then fall back to the first glyph available
	  var space = getGlyphById(font, SPACE_ID) ||
	          getMGlyph(font) ||
	          font.chars[0]

	  // and create a fallback for tab
	  var tabWidth = this._opt.tabSize * space.xadvance
	  this._fallbackSpaceGlyph = space
	  this._fallbackTabGlyph = xtend(space, {
	    x: 0,
      y: 0,
      xadvance: tabWidth,
      id: TAB_ID,
	    xoffset: 0,
      yoffset: 0,
      width: 0,
      height: 0
	  })
  }

  TextLayout.prototype.getGlyph = function (font, id) {
	  var glyph = getGlyphById(font, id)
	  if (glyph) { return glyph } else if (id === TAB_ID) { return this._fallbackTabGlyph } else if (id === SPACE_ID) { return this._fallbackSpaceGlyph }
	  return null
  }

  TextLayout.prototype.computeMetrics = function (text, start, end, width) {
	  var letterSpacing = this._opt.letterSpacing || 0
	  var font = this._opt.font
	  var curPen = 0
	  var curWidth = 0
	  var count = 0
	  var glyph
	  var lastGlyph

	  if (!font.chars || font.chars.length === 0) {
	    return {
	      start: start,
	      end: start,
	      width: 0
	    }
	  }

	  end = Math.min(text.length, end)
	  for (var i = start; i < end; i++) {
	    var id = text.charCodeAt(i)
	    var glyph = this.getGlyph(font, id)

	    if (glyph) {
	      // move pen forward
	      var xoff = glyph.xoffset
	      var kern = lastGlyph ? getKerning(font, lastGlyph.id, glyph.id) : 0
	      curPen += kern

	      var nextPen = curPen + glyph.xadvance + letterSpacing
	      var nextWidth = curPen + glyph.width

	      // we've hit our limit; we can't move onto the next glyph
	      if (nextWidth >= width || nextPen >= width) { break }

	      // otherwise continue along our line
	      curPen = nextPen
	      curWidth = nextWidth
	      lastGlyph = glyph
	    }
	    count++
	  }

	  // make sure rightmost edge lines up with rendered glyphs
	  if (lastGlyph) { curWidth += lastGlyph.xoffset }

	  return {
	    start: start,
	    end: start + count,
	    width: curWidth
	  }
  }

  // getters for the private vars
  ;['width', 'height',
	  'descender', 'ascender',
	  'xHeight', 'baseline',
	  'capHeight',
	  'lineHeight' ].forEach(addGetter)

  function addGetter (name) {
	  Object.defineProperty(TextLayout.prototype, name, {
	    get: wrapper(name),
	    configurable: true
	  })
  }

  // create lookups for private vars
  function wrapper (name) {
	  return (new Function([
	    'return function ' + name + '() {',
	    '  return this._' + name,
	    '}'
	  ].join('\n')))()
  }

  function getGlyphById (font, id) {
	  if (!font.chars || font.chars.length === 0) { return null }

	  var glyphIdx = findChar(font.chars, id)
	  if (glyphIdx >= 0) { return font.chars[glyphIdx] }
	  return null
  }

  function getXHeight (font) {
	  for (var i = 0; i < X_HEIGHTS.length; i++) {
	    var id = X_HEIGHTS[i].charCodeAt(0)
	    var idx = findChar(font.chars, id)
	    if (idx >= 0) { return font.chars[idx].height }
	  }
	  return 0
  }

  function getMGlyph (font) {
	  for (var i = 0; i < M_WIDTHS.length; i++) {
	    var id = M_WIDTHS[i].charCodeAt(0)
	    var idx = findChar(font.chars, id)
	    if (idx >= 0) { return font.chars[idx] }
	  }
	  return 0
  }

  function getCapHeight (font) {
	  for (var i = 0; i < CAP_HEIGHTS.length; i++) {
	    var id = CAP_HEIGHTS[i].charCodeAt(0)
	    var idx = findChar(font.chars, id)
	    if (idx >= 0) { return font.chars[idx].height }
	  }
	  return 0
  }

  function getKerning (font, left, right) {
	  if (!font.kernings || font.kernings.length === 0) { return 0 }

	  var table = font.kernings
	  for (var i = 0; i < table.length; i++) {
	    var kern = table[i]
	    if (kern.first === left && kern.second === right) { return kern.amount }
	  }
	  return 0
  }

  function getAlignType (align) {
	  if (align === 'center') { return ALIGN_CENTER } else if (align === 'right') { return ALIGN_RIGHT }
	  return ALIGN_LEFT
  }
}, {'as-number': 6, 'indexof-property': 7, 'word-wrapper': 8, 'xtend': 9}],
6: [function (require, module, exports) {
  module.exports = function numtype (num, def) {
	  return typeof num === 'number'
	    ? num
	    : (typeof def === 'number' ? def : 0)
  }
}, {}],
7: [function (require, module, exports) {
  module.exports = function compile (property) {
	  if (!property || typeof property !== 'string') { throw new Error('must specify property for indexof search') }

	  return new Function('array', 'value', 'start', [
	    'start = start || 0',
	    'for (var i=start; i<array.length; i++)',
	    '  if (array[i]["' + property + '"] === value)',
	    '      return i',
	    'return -1'
	  ].join('\n'))
  }
}, {}],
8: [function (require, module, exports) {
  var newline = /\n/
  var newlineChar = '\n'
  var whitespace = /\s/

  module.exports = function (text, opt) {
	    var lines = module.exports.lines(text, opt)
	    return lines.map(function (line) {
	        return text.substring(line.start, line.end)
	    }).join('\n')
  }

  module.exports.lines = function wordwrap (text, opt) {
	    opt = opt || {}

	    // zero width results in nothing visible
	    if (opt.width === 0 && opt.mode !== 'nowrap') { return [] }

	    text = text || ''
	    var width = typeof opt.width === 'number' ? opt.width : Number.MAX_VALUE
	    var start = Math.max(0, opt.start || 0)
	    var end = typeof opt.end === 'number' ? opt.end : text.length
	    var mode = opt.mode

	    var measure = opt.measure || monospace
	    if (mode === 'pre') { return pre(measure, text, start, end, width) } else { return greedy(measure, text, start, end, width, mode) }
  }

  function idxOf (text, chr, start, end) {
	    var idx = text.indexOf(chr, start)
	    if (idx === -1 || idx > end) { return end }
	    return idx
  }

  function isWhitespace (chr) {
	    return whitespace.test(chr)
  }

  function pre (measure, text, start, end, width) {
	    var lines = []
	    var lineStart = start
	    for (var i = start; i < end && i < text.length; i++) {
	        var chr = text.charAt(i)
	        var isNewline = newline.test(chr)

	        // If we've reached a newline, then step down a line
	        // Or if we've reached the EOF
	        if (isNewline || i === end - 1) {
	            var lineEnd = isNewline ? i : i + 1
	            var measured = measure(text, lineStart, lineEnd, width)
	            lines.push(measured)

	            lineStart = i + 1
	        }
	    }
	    return lines
  }

  function greedy (measure, text, start, end, width, mode) {
	    // A greedy word wrapper based on LibGDX algorithm
	    // https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/BitmapFontCache.java
	    var lines = []

	    var testWidth = width
	    // if 'nowrap' is specified, we only wrap on newline chars
	    if (mode === 'nowrap') { testWidth = Number.MAX_VALUE }

	    while (start < end && start < text.length) {
	        // get next newline position
	        var newLine = idxOf(text, newlineChar, start, end)

	        // eat whitespace at start of line
	        while (start < newLine) {
	            if (!isWhitespace(text.charAt(start))) { break }
	            start++
	        }

	        // determine visible # of glyphs for the available width
	        var measured = measure(text, start, newLine, testWidth)

	        var lineEnd = start + (measured.end - measured.start)
	        var nextStart = lineEnd + newlineChar.length

	        // if we had to cut the line before the next newline...
	        if (lineEnd < newLine) {
	            // find char to break on
	            while (lineEnd > start) {
	                if (isWhitespace(text.charAt(lineEnd))) { break }
	                lineEnd--
	            }
	            if (lineEnd === start) {
	                if (nextStart > start + newlineChar.length) nextStart--
	                lineEnd = nextStart // If no characters to break, show all.
	            } else {
	                nextStart = lineEnd
	                // eat whitespace at end of line
	                while (lineEnd > start) {
	                    if (!isWhitespace(text.charAt(lineEnd - newlineChar.length))) { break }
	                    lineEnd--
	                }
	            }
	        }
	        if (lineEnd >= start) {
	            var result = measure(text, start, lineEnd, testWidth)
	            lines.push(result)
	        }
	        start = nextStart
	    }
	    return lines
  }

  // determines the visible number of glyphs within a given width
  function monospace (text, start, end, width) {
	    var glyphs = Math.min(width, end - start)
	    return {
	        start: start,
	        end: start + glyphs
	    }
  }
}, {}],
9: [function (require, module, exports) {
  module.exports = extend

  var hasOwnProperty = Object.prototype.hasOwnProperty

  function extend () {
	    var target = {}

	    for (var i = 0; i < arguments.length; i++) {
	        var source = arguments[i]

	        for (var key in source) {
	            if (hasOwnProperty.call(source, key)) {
	                target[key] = source[key]
	            }
	        }
	    }

	    return target
  }
}, {}],
10: [function (require, module, exports) {
  (function (Buffer) {
    var xhr = require('xhr')
    var noop = function () {}
    var parseASCII = require('parse-bmfont-ascii')
    var parseXML = require('parse-bmfont-xml')
    var readBinary = require('parse-bmfont-binary')
    var isBinaryFormat = require('./lib/is-binary')
    var xtend = require('xtend')

    var xml2 = (function hasXML2 () {
	  return window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()
    })()

    module.exports = function (opt, cb) {
	  cb = typeof cb === 'function' ? cb : noop

	  if (typeof opt === 'string') { opt = { uri: opt } } else if (!opt) { opt = {} }

	  var expectBinary = opt.binary
	  if (expectBinary) { opt = getBinaryOpts(opt) }

	  xhr(opt, function (err, res, body) {
	    if (err) { return cb(err) }
	    if (!/^2/.test(res.statusCode)) { return cb(new Error('http status code: ' + res.statusCode)) }
	    if (!body) { return cb(new Error('no body result')) }

	    var binary = false

	    // if the response type is an array buffer,
	    // we need to convert it into a regular Buffer object
	    if (isArrayBuffer(body)) {
	      var array = new Uint8Array(body)
	      body = new Buffer(array, 'binary')
	    }

	    // now check the string/Buffer response
	    // and see if it has a binary BMF header
	    if (isBinaryFormat(body)) {
	      binary = true
	      // if we have a string, turn it into a Buffer
	      if (typeof body === 'string') { body = new Buffer(body, 'binary') }
	    }

	    // we are not parsing a binary format, just ASCII/XML/etc
	    if (!binary) {
	      // might still be a buffer if responseType is 'arraybuffer'
	      if (Buffer.isBuffer(body)) { body = body.toString(opt.encoding) }
	      body = body.trim()
	    }

	    var result
	    try {
	      var type = res.headers['content-type']
	      if (binary) { result = readBinary(body) } else if (/json/.test(type) || body.charAt(0) === '{') { result = JSON.parse(body) } else if (/xml/.test(type) || body.charAt(0) === '<') { result = parseXML(body) } else { result = parseASCII(body) }
	    } catch (e) {
	      cb(new Error('error parsing font ' + e.message))
	      cb = noop
	    }
	    cb(null, result)
	  })
    }

    function isArrayBuffer (arr) {
	  var str = Object.prototype.toString
	  return str.call(arr) === '[object ArrayBuffer]'
    }

    function getBinaryOpts (opt) {
	  // IE10+ and other modern browsers support array buffers
	  if (xml2) { return xtend(opt, { responseType: 'arraybuffer' }) }

	  if (typeof window.XMLHttpRequest === 'undefined') { throw new Error('your browser does not support XHR loading') }

	  // IE9 and XML1 browsers could still use an override
	  var req = new window.XMLHttpRequest()
	  req.overrideMimeType('text/plain; charset=x-user-defined')
	  return xtend({
	    xhr: req
	  }, opt)
    }
  }).call(this, require('buffer').Buffer)
}, {'./lib/is-binary': 11, 'buffer': 37, 'parse-bmfont-ascii': 13, 'parse-bmfont-binary': 14, 'parse-bmfont-xml': 15, 'xhr': 18, 'xtend': 25}],
11: [function (require, module, exports) {
  (function (Buffer) {
    var equal = require('buffer-equal')
    var HEADER = new Buffer([66, 77, 70, 3])

    module.exports = function (buf) {
	  if (typeof buf === 'string') { return buf.substring(0, 3) === 'BMF' }
	  return buf.length > 4 && equal(buf.slice(0, 4), HEADER)
    }
  }).call(this, require('buffer').Buffer)
}, {'buffer': 37, 'buffer-equal': 12}],
12: [function (require, module, exports) {
  var Buffer = require('buffer').Buffer // for use with browserify

  module.exports = function (a, b) {
	    if (!Buffer.isBuffer(a)) return undefined
	    if (!Buffer.isBuffer(b)) return undefined
	    if (typeof a.equals === 'function') return a.equals(b)
	    if (a.length !== b.length) return false

	    for (var i = 0; i < a.length; i++) {
	        if (a[i] !== b[i]) return false
	    }

	    return true
  }
}, {'buffer': 37}],
13: [function (require, module, exports) {
  module.exports = function parseBMFontAscii (data) {
	  if (!data) { throw new Error('no data provided') }
	  data = data.toString().trim()

	  var output = {
	    pages: [],
	    chars: [],
	    kernings: []
	  }

	  var lines = data.split(/\r\n?|\n/g)

	  if (lines.length === 0) { throw new Error('no data in BMFont file') }

	  for (var i = 0; i < lines.length; i++) {
	    var lineData = splitLine(lines[i], i)
	    if (!lineData) // skip empty lines
	      { continue }

	    if (lineData.key === 'page') {
	      if (typeof lineData.data.id !== 'number') { throw new Error('malformed file at line ' + i + ' -- needs page id=N') }
	      if (typeof lineData.data.file !== 'string') { throw new Error('malformed file at line ' + i + ' -- needs page file="path"') }
	      output.pages[lineData.data.id] = lineData.data.file
	    } else if (lineData.key === 'chars' || lineData.key === 'kernings') {
	      // ... do nothing for these two ...
	    } else if (lineData.key === 'char') {
	      output.chars.push(lineData.data)
	    } else if (lineData.key === 'kerning') {
	      output.kernings.push(lineData.data)
	    } else {
	      output[lineData.key] = lineData.data
	    }
	  }

	  return output
  }

  function splitLine (line, idx) {
	  line = line.replace(/\t+/g, ' ').trim()
	  if (!line) { return null }

	  var space = line.indexOf(' ')
	  if (space === -1) { throw new Error('no named row at line ' + idx) }

	  var key = line.substring(0, space)

	  line = line.substring(space + 1)
	  // clear "letter" field as it is non-standard and
	  // requires additional complexity to parse " / = symbols
	  line = line.replace(/letter=[\'\"]\S+[\'\"]/gi, '')
	  line = line.split('=')
	  line = line.map(function (str) {
	    return str.trim().match((/(".*?"|[^"\s]+)+(?=\s*|\s*$)/g))
	  })

	  var data = []
	  for (var i = 0; i < line.length; i++) {
	    var dt = line[i]
	    if (i === 0) {
	      data.push({
	        key: dt[0],
	        data: ''
	      })
	    } else if (i === line.length - 1) {
	      data[data.length - 1].data = parseData(dt[0])
	    } else {
	      data[data.length - 1].data = parseData(dt[0])
	      data.push({
	        key: dt[1],
	        data: ''
	      })
	    }
	  }

	  var out = {
	    key: key,
	    data: {}
	  }

	  data.forEach(function (v) {
	    out.data[v.key] = v.data
	  })

	  return out
  }

  function parseData (data) {
	  if (!data || data.length === 0) { return '' }

	  if (data.indexOf('"') === 0 || data.indexOf("'") === 0) { return data.substring(1, data.length - 1) }
	  if (data.indexOf(',') !== -1) { return parseIntList(data) }
	  return parseInt(data, 10)
  }

  function parseIntList (data) {
	  return data.split(',').map(function (val) {
	    return parseInt(val, 10)
	  })
  }
}, {}],
14: [function (require, module, exports) {
  var HEADER = [66, 77, 70]

  module.exports = function readBMFontBinary (buf) {
	  if (buf.length < 6) { throw new Error('invalid buffer length for BMFont') }

	  var header = HEADER.every(function (byte, i) {
	    return buf.readUInt8(i) === byte
	  })

	  if (!header) { throw new Error('BMFont missing BMF byte header') }

	  var i = 3
	  var vers = buf.readUInt8(i++)
	  if (vers > 3) { throw new Error('Only supports BMFont Binary v3 (BMFont App v1.10)') }

	  var target = { kernings: [], chars: [] }
	  for (var b = 0; b < 5; b++) { i += readBlock(target, buf, i) }
	  return target
  }

  function readBlock (target, buf, i) {
	  if (i > buf.length - 1) { return 0 }

	  var blockID = buf.readUInt8(i++)
	  var blockSize = buf.readInt32LE(i)
	  i += 4

	  switch (blockID) {
	    case 1:
	      target.info = readInfo(buf, i)
	      break
	    case 2:
	      target.common = readCommon(buf, i)
	      break
	    case 3:
	      target.pages = readPages(buf, i, blockSize)
	      break
	    case 4:
	      target.chars = readChars(buf, i, blockSize)
	      break
	    case 5:
	      target.kernings = readKernings(buf, i, blockSize)
	      break
	  }
	  return 5 + blockSize
  }

  function readInfo (buf, i) {
	  var info = {}
	  info.size = buf.readInt16LE(i)

	  var bitField = buf.readUInt8(i + 2)
	  info.smooth = (bitField >> 7) & 1
	  info.unicode = (bitField >> 6) & 1
	  info.italic = (bitField >> 5) & 1
	  info.bold = (bitField >> 4) & 1

	  // fixedHeight is only mentioned in binary spec
	  if ((bitField >> 3) & 1) { info.fixedHeight = 1 }

	  info.charset = buf.readUInt8(i + 3) || ''
	  info.stretchH = buf.readUInt16LE(i + 4)
	  info.aa = buf.readUInt8(i + 6)
	  info.padding = [
	    buf.readInt8(i + 7),
	    buf.readInt8(i + 8),
	    buf.readInt8(i + 9),
	    buf.readInt8(i + 10)
	  ]
	  info.spacing = [
	    buf.readInt8(i + 11),
	    buf.readInt8(i + 12)
	  ]
	  info.outline = buf.readUInt8(i + 13)
	  info.face = readStringNT(buf, i + 14)
	  return info
  }

  function readCommon (buf, i) {
	  var common = {}
	  common.lineHeight = buf.readUInt16LE(i)
	  common.base = buf.readUInt16LE(i + 2)
	  common.scaleW = buf.readUInt16LE(i + 4)
	  common.scaleH = buf.readUInt16LE(i + 6)
	  common.pages = buf.readUInt16LE(i + 8)
	  var bitField = buf.readUInt8(i + 10)
	  common.packed = 0
	  common.alphaChnl = buf.readUInt8(i + 11)
	  common.redChnl = buf.readUInt8(i + 12)
	  common.greenChnl = buf.readUInt8(i + 13)
	  common.blueChnl = buf.readUInt8(i + 14)
	  return common
  }

  function readPages (buf, i, size) {
	  var pages = []
	  var text = readNameNT(buf, i)
	  var len = text.length + 1
	  var count = size / len
	  for (var c = 0; c < count; c++) {
	    pages[c] = buf.slice(i, i + text.length).toString('utf8')
	    i += len
	  }
	  return pages
  }

  function readChars (buf, i, blockSize) {
	  var chars = []

	  var count = blockSize / 20
	  for (var c = 0; c < count; c++) {
	    var char = {}
	    var off = c * 20
	    char.id = buf.readUInt32LE(i + 0 + off)
	    char.x = buf.readUInt16LE(i + 4 + off)
	    char.y = buf.readUInt16LE(i + 6 + off)
	    char.width = buf.readUInt16LE(i + 8 + off)
	    char.height = buf.readUInt16LE(i + 10 + off)
	    char.xoffset = buf.readInt16LE(i + 12 + off)
	    char.yoffset = buf.readInt16LE(i + 14 + off)
	    char.xadvance = buf.readInt16LE(i + 16 + off)
	    char.page = buf.readUInt8(i + 18 + off)
	    char.chnl = buf.readUInt8(i + 19 + off)
	    chars[c] = char
	  }
	  return chars
  }

  function readKernings (buf, i, blockSize) {
	  var kernings = []
	  var count = blockSize / 10
	  for (var c = 0; c < count; c++) {
	    var kern = {}
	    var off = c * 10
	    kern.first = buf.readUInt32LE(i + 0 + off)
	    kern.second = buf.readUInt32LE(i + 4 + off)
	    kern.amount = buf.readInt16LE(i + 8 + off)
	    kernings[c] = kern
	  }
	  return kernings
  }

  function readNameNT (buf, offset) {
	  var pos = offset
	  for (; pos < buf.length; pos++) {
	    if (buf[pos] === 0x00) { break }
	  }
	  return buf.slice(offset, pos)
  }

  function readStringNT (buf, offset) {
	  return readNameNT(buf, offset).toString('utf8')
  }
}, {}],
15: [function (require, module, exports) {
  var parseAttributes = require('./parse-attribs')
  var parseFromString = require('xml-parse-from-string')

  // In some cases element.attribute.nodeName can return
  // all lowercase values.. so we need to map them to the correct
  // case
  var NAME_MAP = {
	  scaleh: 'scaleH',
	  scalew: 'scaleW',
	  stretchh: 'stretchH',
	  lineheight: 'lineHeight',
	  alphachnl: 'alphaChnl',
	  redchnl: 'redChnl',
	  greenchnl: 'greenChnl',
	  bluechnl: 'blueChnl'
  }

  module.exports = function parse (data) {
	  data = data.toString()

	  var xmlRoot = parseFromString(data)
	  var output = {
	    pages: [],
	    chars: [],
	    kernings: []
	  }

	  // get config settings
	  ;['info', 'common'].forEach(function (key) {
	    var element = xmlRoot.getElementsByTagName(key)[0]
	    if (element) { output[key] = parseAttributes(getAttribs(element)) }
	  })

	  // get page info
	  var pageRoot = xmlRoot.getElementsByTagName('pages')[0]
	  if (!pageRoot) { throw new Error('malformed file -- no <pages> element') }
	  var pages = pageRoot.getElementsByTagName('page')
	  for (var i = 0; i < pages.length; i++) {
	    var p = pages[i]
	    var id = parseInt(p.getAttribute('id'), 10)
	    var file = p.getAttribute('file')
	    if (isNaN(id)) { throw new Error('malformed file -- page "id" attribute is NaN') }
	    if (!file) { throw new Error('malformed file -- needs page "file" attribute') }
	    output.pages[parseInt(id, 10)] = file
	  }

	  // get kernings / chars
	  ['chars', 'kernings'].forEach(function (key) {
	    var element = xmlRoot.getElementsByTagName(key)[0]
	    if (!element) { return }
	    var childTag = key.substring(0, key.length - 1)
	    var children = element.getElementsByTagName(childTag)
	    for (var i = 0; i < children.length; i++) {
	      var child = children[i]
	      output[key].push(parseAttributes(getAttribs(child)))
	    }
	  })
	  return output
  }

  function getAttribs (element) {
	  var attribs = getAttribList(element)
	  return attribs.reduce(function (dict, attrib) {
	    var key = mapName(attrib.nodeName)
	    dict[key] = attrib.nodeValue
	    return dict
	  }, {})
  }

  function getAttribList (element) {
	  // IE8+ and modern browsers
	  var attribs = []
	  for (var i = 0; i < element.attributes.length; i++) { attribs.push(element.attributes[i]) }
	  return attribs
  }

  function mapName (nodeName) {
	  return NAME_MAP[nodeName.toLowerCase()] || nodeName
  }
}, {'./parse-attribs': 16, 'xml-parse-from-string': 17}],
16: [function (require, module, exports) {
  // Some versions of GlyphDesigner have a typo
  // that causes some bugs with parsing.
  // Need to confirm with recent version of the software
  // to see whether this is still an issue or not.
  var GLYPH_DESIGNER_ERROR = 'chasrset'

  module.exports = function parseAttributes (obj) {
	  if (GLYPH_DESIGNER_ERROR in obj) {
	    obj['charset'] = obj[GLYPH_DESIGNER_ERROR]
	    delete obj[GLYPH_DESIGNER_ERROR]
	  }

	  for (var k in obj) {
	    if (k === 'face' || k === 'charset') { continue } else if (k === 'padding' || k === 'spacing') { obj[k] = parseIntList(obj[k]) } else { obj[k] = parseInt(obj[k], 10) }
	  }
	  return obj
  }

  function parseIntList (data) {
	  return data.split(',').map(function (val) {
	    return parseInt(val, 10)
	  })
  }
}, {}],
17: [function (require, module, exports) {
  module.exports = (function xmlparser () {
	  // common browsers
	  if (typeof window.DOMParser !== 'undefined') {
	    return function (str) {
	      var parser = new window.DOMParser()
	      return parser.parseFromString(str, 'application/xml')
	    }
	  }

	  // IE8 fallback
	  if (typeof window.ActiveXObject !== 'undefined' &&
	      new window.ActiveXObject('Microsoft.XMLDOM')) {
	    return function (str) {
	      var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM')
	      xmlDoc.async = 'false'
	      xmlDoc.loadXML(str)
	      return xmlDoc
	    }
	  }

	  // last resort fallback
	  return function (str) {
	    var div = document.createElement('div')
	    div.innerHTML = str
	    return div
	  }
  })()
}, {}],
18: [function (require, module, exports) {
  'use strict'
  var window = require('global/window')
  var once = require('once')
  var isFunction = require('is-function')
  var parseHeaders = require('parse-headers')
  var xtend = require('xtend')

  module.exports = createXHR
  createXHR.XMLHttpRequest = window.XMLHttpRequest || noop
  createXHR.XDomainRequest = 'withCredentials' in (new createXHR.XMLHttpRequest()) ? createXHR.XMLHttpRequest : window.XDomainRequest

  forEachArray(['get', 'put', 'post', 'patch', 'head', 'delete'], function (method) {
	    createXHR[method === 'delete' ? 'del' : method] = function (uri, options, callback) {
	        options = initParams(uri, options, callback)
	        options.method = method.toUpperCase()
	        return _createXHR(options)
	    }
  })

  function forEachArray (array, iterator) {
	    for (var i = 0; i < array.length; i++) {
	        iterator(array[i])
	    }
  }

  function isEmpty (obj) {
	    for (var i in obj) {
	        if (obj.hasOwnProperty(i)) return false
	    }
	    return true
  }

  function initParams (uri, options, callback) {
	    var params = uri

	    if (isFunction(options)) {
	        callback = options
	        if (typeof uri === 'string') {
	            params = {uri: uri}
	        }
	    } else {
	        params = xtend(options, {uri: uri})
	    }

	    params.callback = callback
	    return params
  }

  function createXHR (uri, options, callback) {
	    options = initParams(uri, options, callback)
	    return _createXHR(options)
  }

  function _createXHR (options) {
	    var callback = options.callback
	    if (typeof callback === 'undefined') {
	        throw new Error('callback argument missing')
	    }
	    callback = once(callback)

	    function readystatechange () {
	        if (xhr.readyState === 4) {
	            loadFunc()
	        }
	    }

	    function getBody () {
	        // Chrome with requestType=blob throws errors arround when even testing access to responseText
	        var body = undefined

	        if (xhr.response) {
	            body = xhr.response
	        } else if (xhr.responseType === 'text' || !xhr.responseType) {
	            body = xhr.responseText || xhr.responseXML
	        }

	        if (isJson) {
	            try {
	                body = JSON.parse(body)
	            } catch (e) {}
	        }

	        return body
	    }

	    var failureResponse = {
	                body: undefined,
	                headers: {},
	                statusCode: 0,
	                method: method,
	                url: uri,
	                rawRequest: xhr
	            }

	    function errorFunc (evt) {
	        clearTimeout(timeoutTimer)
	        if (!(evt instanceof Error)) {
	            evt = new Error('' + (evt || 'Unknown XMLHttpRequest Error'))
	        }
	        evt.statusCode = 0
	        callback(evt, failureResponse)
	    }

	    // will load the data & process the response in a special response object
	    function loadFunc () {
	        if (aborted) return
	        var status
	        clearTimeout(timeoutTimer)
	        if (options.useXDR && xhr.status === undefined) {
	            // IE8 CORS GET successful response doesn't have a status field, but body is fine
	            status = 200
	        } else {
	            status = (xhr.status === 1223 ? 204 : xhr.status)
	        }
	        var response = failureResponse
	        var err = null

	        if (status !== 0) {
	            response = {
	                body: getBody(),
	                statusCode: status,
	                method: method,
	                headers: {},
	                url: uri,
	                rawRequest: xhr
	            }
	            if (xhr.getAllResponseHeaders) { // remember xhr can in fact be XDR for CORS in IE
	                response.headers = parseHeaders(xhr.getAllResponseHeaders())
	            }
	        } else {
	            err = new Error('Internal XMLHttpRequest Error')
	        }
	        callback(err, response, response.body)
	    }

	    var xhr = options.xhr || null

	    if (!xhr) {
	        if (options.cors || options.useXDR) {
	            xhr = new createXHR.XDomainRequest()
	        } else {
	            xhr = new createXHR.XMLHttpRequest()
	        }
	    }

	    var key
	    var aborted
	    var uri = xhr.url = options.uri || options.url
	    var method = xhr.method = options.method || 'GET'
	    var body = options.body || options.data || null
	    var headers = xhr.headers = options.headers || {}
	    var sync = !!options.sync
	    var isJson = false
	    var timeoutTimer

	    if ('json' in options) {
	        isJson = true
	        headers['accept'] || headers['Accept'] || (headers['Accept'] = 'application/json') // Don't override existing accept header declared by user
	        if (method !== 'GET' && method !== 'HEAD') {
	            headers['content-type'] || headers['Content-Type'] || (headers['Content-Type'] = 'application/json') // Don't override existing accept header declared by user
	            body = JSON.stringify(options.json)
	        }
	    }

	    xhr.onreadystatechange = readystatechange
	    xhr.onload = loadFunc
	    xhr.onerror = errorFunc
	    // IE9 must have onprogress be set to a unique function.
	    xhr.onprogress = function () {
	        // IE must die
	    }
	    xhr.ontimeout = errorFunc
	    xhr.open(method, uri, !sync, options.username, options.password)
	    // has to be after open
	    if (!sync) {
	        xhr.withCredentials = !!options.withCredentials
	    }
	    // Cannot set timeout with sync request
	    // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly
	    // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
	    if (!sync && options.timeout > 0) {
	        timeoutTimer = setTimeout(function () {
	            aborted = true// IE9 may still call readystatechange
	            xhr.abort('timeout')
	            var e = new Error('XMLHttpRequest timeout')
	            e.code = 'ETIMEDOUT'
	            errorFunc(e)
	        }, options.timeout)
	    }

	    if (xhr.setRequestHeader) {
	        for (key in headers) {
	            if (headers.hasOwnProperty(key)) {
	                xhr.setRequestHeader(key, headers[key])
	            }
	        }
	    } else if (options.headers && !isEmpty(options.headers)) {
	        throw new Error('Headers cannot be set on an XDomainRequest object')
	    }

	    if ('responseType' in options) {
	        xhr.responseType = options.responseType
	    }

	    if ('beforeSend' in options &&
	        typeof options.beforeSend === 'function'
	    ) {
	        options.beforeSend(xhr)
	    }

	    xhr.send(body)

	    return xhr
  }

  function noop () {}
}, {'global/window': 19, 'is-function': 20, 'once': 21, 'parse-headers': 24, 'xtend': 25}],
19: [function (require, module, exports) {
  (function (global) {
    if (typeof window !== 'undefined') {
	    module.exports = window
    } else if (typeof global !== 'undefined') {
	    module.exports = global
    } else if (typeof self !== 'undefined') {
	    module.exports = self
    } else {
	    module.exports = {}
    }
  }).call(this, typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {})
}, {}],
20: [function (require, module, exports) {
  module.exports = isFunction

  var toString = Object.prototype.toString

  function isFunction (fn) {
	  var string = toString.call(fn)
	  return string === '[object Function]' ||
	    (typeof fn === 'function' && string !== '[object RegExp]') ||
	    (typeof window !== 'undefined' &&
	     // IE8 and below
	     (fn === window.setTimeout ||
	      fn === window.alert ||
	      fn === window.confirm ||
	      fn === window.prompt))
  }
}, {}],
21: [function (require, module, exports) {
  module.exports = once

  once.proto = once(function () {
	  Object.defineProperty(Function.prototype, 'once', {
	    value: function () {
	      return once(this)
	    },
	    configurable: true
	  })
  })

  function once (fn) {
	  var called = false
	  return function () {
	    if (called) return
	    called = true
	    return fn.apply(this, arguments)
	  }
  }
}, {}],
22: [function (require, module, exports) {
  var isFunction = require('is-function')

  module.exports = forEach

  var toString = Object.prototype.toString
  var hasOwnProperty = Object.prototype.hasOwnProperty

  function forEach (list, iterator, context) {
	    if (!isFunction(iterator)) {
	        throw new TypeError('iterator must be a function')
	    }

	    if (arguments.length < 3) {
	        context = this
	    }

	    if (toString.call(list) === '[object Array]') { forEachArray(list, iterator, context) } else if (typeof list === 'string') { forEachString(list, iterator, context) } else { forEachObject(list, iterator, context) }
  }

  function forEachArray (array, iterator, context) {
	    for (var i = 0, len = array.length; i < len; i++) {
	        if (hasOwnProperty.call(array, i)) {
	            iterator.call(context, array[i], i, array)
	        }
	    }
  }

  function forEachString (string, iterator, context) {
	    for (var i = 0, len = string.length; i < len; i++) {
	        // no such thing as a sparse string.
	        iterator.call(context, string.charAt(i), i, string)
	    }
  }

  function forEachObject (object, iterator, context) {
	    for (var k in object) {
	        if (hasOwnProperty.call(object, k)) {
	            iterator.call(context, object[k], k, object)
	        }
	    }
  }
}, {'is-function': 20}],
23: [function (require, module, exports) {
  exports = module.exports = trim

  function trim (str) {
	  return str.replace(/^\s*|\s*$/g, '')
  }

  exports.left = function (str) {
	  return str.replace(/^\s*/, '')
  }

  exports.right = function (str) {
	  return str.replace(/\s*$/, '')
  }
}, {}],
24: [function (require, module, exports) {
  var trim = require('trim'),
	   forEach = require('for-each'),
	   isArray = function (arg) {
	      return Object.prototype.toString.call(arg) === '[object Array]'
	    }

  module.exports = function (headers) {
	  if (!headers) { return {} }

	  var result = {}

	  forEach(
	      trim(headers).split('\n')
	    , function (row) {
	        var index = row.indexOf(':'),
	           key = trim(row.slice(0, index)).toLowerCase(),
	           value = trim(row.slice(index + 1))

	        if (typeof (result[key]) === 'undefined') {
	          result[key] = value
	        } else if (isArray(result[key])) {
	          result[key].push(value)
	        } else {
	          result[key] = [ result[key], value ]
	        }
	      }
	  )

	  return result
  }
}, {'for-each': 22, 'trim': 23}],
25: [function (require, module, exports) {
  arguments[4][9][0].apply(exports, arguments)
}, {'dup': 9}],
26: [function (require, module, exports) {
  /* eslint-disable no-unused-vars */
  'use strict'
  var hasOwnProperty = Object.prototype.hasOwnProperty
  var propIsEnumerable = Object.prototype.propertyIsEnumerable

  function toObject (val) {
	  if (val === null || val === undefined) {
	    throw new TypeError('Object.assign cannot be called with null or undefined')
	  }

	  return Object(val)
  }

  module.exports = Object.assign || function (target, source) {
	  var from
	  var to = toObject(target)
	  var symbols

	  for (var s = 1; s < arguments.length; s++) {
	    from = Object(arguments[s])

	    for (var key in from) {
	      if (hasOwnProperty.call(from, key)) {
	        to[key] = from[key]
	      }
	    }

	    if (Object.getOwnPropertySymbols) {
	      symbols = Object.getOwnPropertySymbols(from)
	      for (var i = 0; i < symbols.length; i++) {
	        if (propIsEnumerable.call(from, symbols[i])) {
	          to[symbols[i]] = from[symbols[i]]
	        }
	      }
	    }
	  }

	  return to
  }
}, {}],
27: [function (require, module, exports) {
  var dtype = require('dtype')
  var anArray = require('an-array')
  var isBuffer = require('is-buffer')

  var CW = [0, 2, 3]
  var CCW = [2, 1, 3]

  module.exports = function createQuadElements (array, opt) {
	    // if user didn't specify an output array
	    if (!array || !(anArray(array) || isBuffer(array))) {
	        opt = array || {}
	        array = null
	    }

	    if (typeof opt === 'number') // backwards-compatible
	        { opt = { count: opt } } else { opt = opt || {} }

	    var type = typeof opt.type === 'string' ? opt.type : 'uint16'
	    var count = typeof opt.count === 'number' ? opt.count : 1
	    var start = (opt.start || 0)

	    var dir = opt.clockwise !== false ? CW : CCW,
	        a = dir[0],
	        b = dir[1],
	        c = dir[2]

	    var numIndices = count * 6

	    var indices = array || new (dtype(type))(numIndices)
	    for (var i = 0, j = 0; i < numIndices; i += 6, j += 4) {
	        var x = i + start
	        indices[x + 0] = j + 0
	        indices[x + 1] = j + 1
	        indices[x + 2] = j + 2
	        indices[x + 3] = j + a
	        indices[x + 4] = j + b
	        indices[x + 5] = j + c
	    }
	    return indices
  }
}, {'an-array': 28, 'dtype': 29, 'is-buffer': 30}],
28: [function (require, module, exports) {
  var str = Object.prototype.toString

  module.exports = anArray

  function anArray (arr) {
	  return (
	       arr.BYTES_PER_ELEMENT &&
	    str.call(arr.buffer) === '[object ArrayBuffer]' ||
	    Array.isArray(arr)
	  )
  }
}, {}],
29: [function (require, module, exports) {
  module.exports = function (dtype) {
	  switch (dtype) {
	    case 'int8':
	      return Int8Array
	    case 'int16':
	      return Int16Array
	    case 'int32':
	      return Int32Array
	    case 'uint8':
	      return Uint8Array
	    case 'uint16':
	      return Uint16Array
	    case 'uint32':
	      return Uint32Array
	    case 'float32':
	      return Float32Array
	    case 'float64':
	      return Float64Array
	    case 'array':
	      return Array
	    case 'uint8_clamped':
	      return Uint8ClampedArray
	  }
  }
}, {}],
30: [function (require, module, exports) {
  /**
	 * Determine if an object is Buffer
	 *
	 * Author:   Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
	 * License:  MIT
	 *
	 * `npm install is-buffer`
	 */

  module.exports = function (obj) {
	  return !!(obj != null &&
	    (obj._isBuffer || // For Safari 5-7 (missing Object.prototype.constructor)
	      (obj.constructor &&
	      typeof obj.constructor.isBuffer === 'function' &&
	      obj.constructor.isBuffer(obj))
	    ))
  }
}, {}],
31: [function (require, module, exports) {
  var flatten = require('flatten-vertex-data')

  module.exports.attr = setAttribute
  module.exports.index = setIndex

  function setIndex (geometry, data, itemSize, dtype) {
	  if (typeof itemSize !== 'number') itemSize = 1
	  if (typeof dtype !== 'number') dtype = 'uint16'

	  var isR69 = !geometry.index && typeof geometry.setIndex !== 'function'
	  var attrib = isR69 ? geometry.getAttribute('index') : geometry.index
	  var newAttrib = updateAttribute(attrib, data, itemSize, dtype)
	  if (newAttrib) {
	    if (isR69) geometry.addAttribute('index', newAttrib)
	    else geometry.index = newAttrib
	  }
  }

  function setAttribute (geometry, key, data, itemSize, dtype) {
	  if (typeof itemSize !== 'number') itemSize = 3
	  if (typeof dtype !== 'number') dtype = 'float32'
	  if (Array.isArray(data) &&
	    Array.isArray(data[0]) &&
	    data[0].length !== itemSize) {
	    throw new Error('Nested vertex array has unexpected size; expected ' +
	      itemSize + ' but found ' + data[0].length)
	  }

	  var attrib = geometry.getAttribute(key)
	  var newAttrib = updateAttribute(attrib, data, itemSize, dtype)
	  if (newAttrib) {
	    geometry.addAttribute(key, newAttrib)
	  }
  }

  function updateAttribute (attrib, data, itemSize, dtype) {
	  data = data || []
	  if (!attrib || rebuildAttribute(attrib, data, itemSize)) {
	    // create a new array with desired type
	    data = flatten(data, dtype)
	    attrib = new THREE.BufferAttribute(data, itemSize)
	    attrib.needsUpdate = true
	    return attrib
	  } else {
	    // copy data into the existing array
	    flatten(data, attrib.array)
	    attrib.needsUpdate = true
	    return null
	  }
  }

  // Test whether the attribute needs to be re-created,
  // returns false if we can re-use it as-is.
  function rebuildAttribute (attrib, data, itemSize) {
	  if (attrib.itemSize !== itemSize) return true
	  if (!attrib.array) return true
	  var attribLength = attrib.array.length
	  if (Array.isArray(data) && Array.isArray(data[0])) {
	    // [ [ x, y, z ] ]
	    return attribLength !== data.length * itemSize
	  } else {
	    // [ x, y, z ]
	    return attribLength !== data.length
	  }
  }
}, {'flatten-vertex-data': 32}],
32: [function (require, module, exports) {
  /* eslint new-cap:0 */
  var dtype = require('dtype')
  module.exports = flattenVertexData
  function flattenVertexData (data, output, offset) {
	  if (!data) throw new TypeError('must specify data as first parameter')
	  offset = +(offset || 0) | 0

	  if (Array.isArray(data) && Array.isArray(data[0])) {
	    var dim = data[0].length
	    var length = data.length * dim

	    // no output specified, create a new typed array
	    if (!output || typeof output === 'string') {
	      output = new (dtype(output || 'float32'))(length + offset)
	    }

	    var dstLength = output.length - offset
	    if (length !== dstLength) {
	      throw new Error('source length ' + length + ' (' + dim + 'x' + data.length + ')' +
	        ' does not match destination length ' + dstLength)
	    }

	    for (var i = 0, k = offset; i < data.length; i++) {
	      for (var j = 0; j < dim; j++) {
	        output[k++] = data[i][j]
	      }
	    }
	  } else {
	    if (!output || typeof output === 'string') {
	      // no output, create a new one
	      var Ctor = dtype(output || 'float32')
	      if (offset === 0) {
	        output = new Ctor(data)
	      } else {
	        output = new Ctor(data.length + offset)
	        output.set(data, offset)
	      }
	    } else {
	      // store output in existing array
	      output.set(data, offset)
	    }
	  }

	  return output
  }
}, {'dtype': 33}],
33: [function (require, module, exports) {
  arguments[4][29][0].apply(exports, arguments)
}, {'dup': 29}],
34: [function (require, module, exports) {
  var assign = require('object-assign')

  module.exports = function createSDFShader (opt) {
	  opt = opt || {}
	  var opacity = typeof opt.opacity === 'number' ? opt.opacity : 1
	  var alphaTest = typeof opt.alphaTest === 'number' ? opt.alphaTest : 0.0001
	  var precision = opt.precision || 'highp'
	  var color = opt.color
	  var map = opt.map

	  // remove to satisfy r73
	  delete opt.map
	  delete opt.color
	  delete opt.precision
	  delete opt.opacity

	  return assign({
	    uniforms: {
	      opacity: { type: 'f', value: opacity },
	      map: { type: 't', value: map || new THREE.Texture() },
	      color: { type: 'c', value: new THREE.Color(color) }
	    },
	    vertexShader: [
	      'attribute vec2 uv;',
	      'attribute vec4 position;',
	      'uniform mat4 projectionMatrix;',
	      'uniform mat4 modelViewMatrix;',
	      'varying vec2 vUv;',
	      'void main() {',
	      'vUv = uv;',
	      'gl_Position = projectionMatrix * modelViewMatrix * position;',
	      '}'
	    ].join('\n'),
	    fragmentShader: [
	      '#ifdef GL_OES_standard_derivatives',
	      '#extension GL_OES_standard_derivatives : enable',
	      '#endif',
	      'precision ' + precision + ' float;',
	      'uniform float opacity;',
	      'uniform vec3 color;',
	      'uniform sampler2D map;',
	      'varying vec2 vUv;',

	      'float aastep(float value) {',
	      '  #ifdef GL_OES_standard_derivatives',
	      '    float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757;',
	      '  #else',
	      '    float afwidth = (1.0 / 32.0) * (1.4142135623730951 / (2.0 * gl_FragCoord.w));',
	      '  #endif',
	      '  return smoothstep(0.5 - afwidth, 0.5 + afwidth, value);',
	      '}',

	      'void main() {',
	      '  vec4 texColor = texture2D(map, vUv);',
	      '  float alpha = aastep(texColor.a);',
	      '  gl_FragColor = vec4(color, opacity * alpha);',
	      alphaTest === 0
	        ? ''
	        : '  if (gl_FragColor.a < ' + alphaTest + ') discard;',
	      '}'
	    ].join('\n')
	  }, opt)
  }
}, {'object-assign': 26}],
35: [function (require, module, exports) {
  var loadFont = require('load-bmfont')
  // global.THREE = require('three')

  // A utility to load a font, then a texture
  module.exports = function (opt, cb) {
	  loadFont(opt.font, function (err, font) {
	    if (err) throw err
      PANOLENS.Utils.TextureLoader.load(opt.image, function (tex) {
	      cb(font, tex)
	    })
	  })
  }
}, {'load-bmfont': 10}],
36: [function (require, module, exports) {
  var createText = require('../')
  var SDFShader = require('../shaders/sdf')

  if (PANOLENS && PANOLENS.Utils && PANOLENS.SpriteText) {
    PANOLENS.Utils.loadBMFont = function (fontObject, callback) {
      require('./load')(fontObject, PANOLENS.SpriteText.prototype.setBMFont.bind(PANOLENS.SpriteText.prototype, callback))
    }
    PANOLENS.SpriteText.prototype.generateTextGeometry = createText
    PANOLENS.SpriteText.prototype.generateSDFShader = SDFShader
  }
}, {'../': 1, '../shaders/sdf': 34, './load': 35}],
37: [function (require, module, exports) {
  (function (global) {
    /*!
	 * The buffer module from node.js, for the browser.
	 *
	 * @author   Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
	 * @license  MIT
	 */
    /* eslint-disable no-proto */

    'use strict'

    var base64 = require('base64-js')
    var ieee754 = require('ieee754')
    var isArray = require('isarray')

    exports.Buffer = Buffer
    exports.SlowBuffer = SlowBuffer
    exports.INSPECT_MAX_BYTES = 50
    Buffer.poolSize = 8192 // not used by this implementation

    var rootParent = {}

    /**
	 * If `Buffer.TYPED_ARRAY_SUPPORT`:
	 *   === true    Use Uint8Array implementation (fastest)
	 *   === false   Use Object implementation (most compatible, even IE6)
	 *
	 * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,
	 * Opera 11.6+, iOS 4.2+.
	 *
	 * Due to various browser bugs, sometimes the Object implementation will be used even
	 * when the browser supports typed arrays.
	 *
	 * Note:
	 *
	 *   - Firefox 4-29 lacks support for adding new properties to `Uint8Array` instances,
	 *     See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438.
	 *
	 *   - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function.
	 *
	 *   - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of
	 *     incorrect length in some situations.

	 * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they
	 * get the Object implementation, which is slower but behaves correctly.
	 */
    Buffer.TYPED_ARRAY_SUPPORT = global.TYPED_ARRAY_SUPPORT !== undefined
	  ? global.TYPED_ARRAY_SUPPORT
	  : typedArraySupport()

    function typedArraySupport () {
	  try {
	    var arr = new Uint8Array(1)
	    arr.foo = function () { return 42 }
	    return arr.foo() === 42 && // typed array instances can be augmented
	        typeof arr.subarray === 'function' && // chrome 9-10 lack `subarray`
	        arr.subarray(1, 1).byteLength === 0 // ie10 has broken `subarray`
	  } catch (e) {
	    return false
	  }
    }

    function kMaxLength () {
	  return Buffer.TYPED_ARRAY_SUPPORT
	    ? 0x7fffffff
	    : 0x3fffffff
    }

    /**
	 * Class: Buffer
	 * =============
	 *
	 * The Buffer constructor returns instances of `Uint8Array` that are augmented
	 * with function properties for all the node `Buffer` API functions. We use
	 * `Uint8Array` so that square bracket notation works as expected -- it returns
	 * a single octet.
	 *
	 * By augmenting the instances, we can avoid modifying the `Uint8Array`
	 * prototype.
	 */
    function Buffer (arg) {
	  if (!(this instanceof Buffer)) {
	    // Avoid going through an ArgumentsAdaptorTrampoline in the common case.
	    if (arguments.length > 1) return new Buffer(arg, arguments[1])
	    return new Buffer(arg)
	  }

	  if (!Buffer.TYPED_ARRAY_SUPPORT) {
	    this.length = 0
	    this.parent = undefined
	  }

	  // Common case.
	  if (typeof arg === 'number') {
	    return fromNumber(this, arg)
	  }

	  // Slightly less common case.
	  if (typeof arg === 'string') {
	    return fromString(this, arg, arguments.length > 1 ? arguments[1] : 'utf8')
	  }

	  // Unusual.
	  return fromObject(this, arg)
    }

    function fromNumber (that, length) {
	  that = allocate(that, length < 0 ? 0 : checked(length) | 0)
	  if (!Buffer.TYPED_ARRAY_SUPPORT) {
	    for (var i = 0; i < length; i++) {
	      that[i] = 0
	    }
	  }
	  return that
    }

    function fromString (that, string, encoding) {
	  if (typeof encoding !== 'string' || encoding === '') encoding = 'utf8'

	  // Assumption: byteLength() return value is always < kMaxLength.
	  var length = byteLength(string, encoding) | 0
	  that = allocate(that, length)

	  that.write(string, encoding)
	  return that
    }

    function fromObject (that, object) {
	  if (Buffer.isBuffer(object)) return fromBuffer(that, object)

	  if (isArray(object)) return fromArray(that, object)

	  if (object == null) {
	    throw new TypeError('must start with number, buffer, array or string')
	  }

	  if (typeof ArrayBuffer !== 'undefined') {
	    if (object.buffer instanceof ArrayBuffer) {
	      return fromTypedArray(that, object)
	    }
	    if (object instanceof ArrayBuffer) {
	      return fromArrayBuffer(that, object)
	    }
	  }

	  if (object.length) return fromArrayLike(that, object)

	  return fromJsonObject(that, object)
    }

    function fromBuffer (that, buffer) {
	  var length = checked(buffer.length) | 0
	  that = allocate(that, length)
	  buffer.copy(that, 0, 0, length)
	  return that
    }

    function fromArray (that, array) {
	  var length = checked(array.length) | 0
	  that = allocate(that, length)
	  for (var i = 0; i < length; i += 1) {
	    that[i] = array[i] & 255
	  }
	  return that
    }

    // Duplicate of fromArray() to keep fromArray() monomorphic.
    function fromTypedArray (that, array) {
	  var length = checked(array.length) | 0
	  that = allocate(that, length)
	  // Truncating the elements is probably not what people expect from typed
	  // arrays with BYTES_PER_ELEMENT > 1 but it's compatible with the behavior
	  // of the old Buffer constructor.
	  for (var i = 0; i < length; i += 1) {
	    that[i] = array[i] & 255
	  }
	  return that
    }

    function fromArrayBuffer (that, array) {
	  // array.byteLength // this throws if `array` is not a valid ArrayBuffer

	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    // Return an augmented `Uint8Array` instance, for best performance
	    that = new Uint8Array(array)
	    that.__proto__ = Buffer.prototype
	  } else {
	    // Fallback: Return an object instance of the Buffer class
	    that = fromTypedArray(that, new Uint8Array(array))
	  }
	  return that
    }

    function fromArrayLike (that, array) {
	  var length = checked(array.length) | 0
	  that = allocate(that, length)
	  for (var i = 0; i < length; i += 1) {
	    that[i] = array[i] & 255
	  }
	  return that
    }

    // Deserialize { type: 'Buffer', data: [1,2,3,...] } into a Buffer object.
    // Returns a zero-length buffer for inputs that don't conform to the spec.
    function fromJsonObject (that, object) {
	  var array
	  var length = 0

	  if (object.type === 'Buffer' && isArray(object.data)) {
	    array = object.data
	    length = checked(array.length) | 0
	  }
	  that = allocate(that, length)

	  for (var i = 0; i < length; i += 1) {
	    that[i] = array[i] & 255
	  }
	  return that
    }

    if (Buffer.TYPED_ARRAY_SUPPORT) {
	  Buffer.prototype.__proto__ = Uint8Array.prototype
	  Buffer.__proto__ = Uint8Array
    } else {
	  // pre-set for values that may exist in the future
	  Buffer.prototype.length = undefined
	  Buffer.prototype.parent = undefined
    }

    function allocate (that, length) {
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    // Return an augmented `Uint8Array` instance, for best performance
	    that = new Uint8Array(length)
	    that.__proto__ = Buffer.prototype
	  } else {
	    // Fallback: Return an object instance of the Buffer class
	    that.length = length
	  }

	  var fromPool = length !== 0 && length <= Buffer.poolSize >>> 1
	  if (fromPool) that.parent = rootParent

	  return that
    }

    function checked (length) {
	  // Note: cannot use `length < kMaxLength` here because that fails when
	  // length is NaN (which is otherwise coerced to zero.)
	  if (length >= kMaxLength()) {
	    throw new RangeError('Attempt to allocate Buffer larger than maximum ' +
	                         'size: 0x' + kMaxLength().toString(16) + ' bytes')
	  }
	  return length | 0
    }

    function SlowBuffer (subject, encoding) {
	  if (!(this instanceof SlowBuffer)) return new SlowBuffer(subject, encoding)

	  var buf = new Buffer(subject, encoding)
	  delete buf.parent
	  return buf
    }

    Buffer.isBuffer = function isBuffer (b) {
	  return !!(b != null && b._isBuffer)
    }

    Buffer.compare = function compare (a, b) {
	  if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) {
	    throw new TypeError('Arguments must be Buffers')
	  }

	  if (a === b) return 0

	  var x = a.length
	  var y = b.length

	  var i = 0
	  var len = Math.min(x, y)
	  while (i < len) {
	    if (a[i] !== b[i]) break

	    ++i
	  }

	  if (i !== len) {
	    x = a[i]
	    y = b[i]
	  }

	  if (x < y) return -1
	  if (y < x) return 1
	  return 0
    }

    Buffer.isEncoding = function isEncoding (encoding) {
	  switch (String(encoding).toLowerCase()) {
	    case 'hex':
	    case 'utf8':
	    case 'utf-8':
	    case 'ascii':
	    case 'binary':
	    case 'base64':
	    case 'raw':
	    case 'ucs2':
	    case 'ucs-2':
	    case 'utf16le':
	    case 'utf-16le':
	      return true
	    default:
	      return false
	  }
    }

    Buffer.concat = function concat (list, length) {
	  if (!isArray(list)) throw new TypeError('list argument must be an Array of Buffers.')

	  if (list.length === 0) {
	    return new Buffer(0)
	  }

	  var i
	  if (length === undefined) {
	    length = 0
	    for (i = 0; i < list.length; i++) {
	      length += list[i].length
	    }
	  }

	  var buf = new Buffer(length)
	  var pos = 0
	  for (i = 0; i < list.length; i++) {
	    var item = list[i]
	    item.copy(buf, pos)
	    pos += item.length
	  }
	  return buf
    }

    function byteLength (string, encoding) {
	  if (typeof string !== 'string') string = '' + string

	  var len = string.length
	  if (len === 0) return 0

	  // Use a for loop to avoid recursion
	  var loweredCase = false
	  for (;;) {
	    switch (encoding) {
	      case 'ascii':
	      case 'binary':
	      // Deprecated
	      case 'raw':
	      case 'raws':
	        return len
	      case 'utf8':
	      case 'utf-8':
	        return utf8ToBytes(string).length
	      case 'ucs2':
	      case 'ucs-2':
	      case 'utf16le':
	      case 'utf-16le':
	        return len * 2
	      case 'hex':
	        return len >>> 1
	      case 'base64':
	        return base64ToBytes(string).length
	      default:
	        if (loweredCase) return utf8ToBytes(string).length // assume utf8
	        encoding = ('' + encoding).toLowerCase()
	        loweredCase = true
	    }
	  }
    }
    Buffer.byteLength = byteLength

    function slowToString (encoding, start, end) {
	  var loweredCase = false

	  start = start | 0
	  end = end === undefined || end === Infinity ? this.length : end | 0

	  if (!encoding) encoding = 'utf8'
	  if (start < 0) start = 0
	  if (end > this.length) end = this.length
	  if (end <= start) return ''

	  while (true) {
	    switch (encoding) {
	      case 'hex':
	        return hexSlice(this, start, end)

	      case 'utf8':
	      case 'utf-8':
	        return utf8Slice(this, start, end)

	      case 'ascii':
	        return asciiSlice(this, start, end)

	      case 'binary':
	        return binarySlice(this, start, end)

	      case 'base64':
	        return base64Slice(this, start, end)

	      case 'ucs2':
	      case 'ucs-2':
	      case 'utf16le':
	      case 'utf-16le':
	        return utf16leSlice(this, start, end)

	      default:
	        if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
	        encoding = (encoding + '').toLowerCase()
	        loweredCase = true
	    }
	  }
    }

    // Even though this property is private, it shouldn't be removed because it is
    // used by `is-buffer` to detect buffer instances in Safari 5-7.
    Buffer.prototype._isBuffer = true

    Buffer.prototype.toString = function toString () {
	  var length = this.length | 0
	  if (length === 0) return ''
	  if (arguments.length === 0) return utf8Slice(this, 0, length)
	  return slowToString.apply(this, arguments)
    }

    Buffer.prototype.equals = function equals (b) {
	  if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
	  if (this === b) return true
	  return Buffer.compare(this, b) === 0
    }

    Buffer.prototype.inspect = function inspect () {
	  var str = ''
	  var max = exports.INSPECT_MAX_BYTES
	  if (this.length > 0) {
	    str = this.toString('hex', 0, max).match(/.{2}/g).join(' ')
	    if (this.length > max) str += ' ... '
	  }
	  return '<Buffer ' + str + '>'
    }

    Buffer.prototype.compare = function compare (b) {
	  if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
	  if (this === b) return 0
	  return Buffer.compare(this, b)
    }

    Buffer.prototype.indexOf = function indexOf (val, byteOffset) {
	  if (byteOffset > 0x7fffffff) byteOffset = 0x7fffffff
	  else if (byteOffset < -0x80000000) byteOffset = -0x80000000
	  byteOffset >>= 0

	  if (this.length === 0) return -1
	  if (byteOffset >= this.length) return -1

	  // Negative offsets start from the end of the buffer
	  if (byteOffset < 0) byteOffset = Math.max(this.length + byteOffset, 0)

	  if (typeof val === 'string') {
	    if (val.length === 0) return -1 // special case: looking for empty string always fails
	    return String.prototype.indexOf.call(this, val, byteOffset)
	  }
	  if (Buffer.isBuffer(val)) {
	    return arrayIndexOf(this, val, byteOffset)
	  }
	  if (typeof val === 'number') {
	    if (Buffer.TYPED_ARRAY_SUPPORT && Uint8Array.prototype.indexOf === 'function') {
	      return Uint8Array.prototype.indexOf.call(this, val, byteOffset)
	    }
	    return arrayIndexOf(this, [ val ], byteOffset)
	  }

	  function arrayIndexOf (arr, val, byteOffset) {
	    var foundIndex = -1
	    for (var i = 0; byteOffset + i < arr.length; i++) {
	      if (arr[byteOffset + i] === val[foundIndex === -1 ? 0 : i - foundIndex]) {
	        if (foundIndex === -1) foundIndex = i
	        if (i - foundIndex + 1 === val.length) return byteOffset + foundIndex
	      } else {
	        foundIndex = -1
	      }
	    }
	    return -1
	  }

	  throw new TypeError('val must be string, number or Buffer')
    }

    function hexWrite (buf, string, offset, length) {
	  offset = Number(offset) || 0
	  var remaining = buf.length - offset
	  if (!length) {
	    length = remaining
	  } else {
	    length = Number(length)
	    if (length > remaining) {
	      length = remaining
	    }
	  }

	  // must be an even number of digits
	  var strLen = string.length
	  if (strLen % 2 !== 0) throw new Error('Invalid hex string')

	  if (length > strLen / 2) {
	    length = strLen / 2
	  }
	  for (var i = 0; i < length; i++) {
	    var parsed = parseInt(string.substr(i * 2, 2), 16)
	    if (isNaN(parsed)) throw new Error('Invalid hex string')
	    buf[offset + i] = parsed
	  }
	  return i
    }

    function utf8Write (buf, string, offset, length) {
	  return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length)
    }

    function asciiWrite (buf, string, offset, length) {
	  return blitBuffer(asciiToBytes(string), buf, offset, length)
    }

    function binaryWrite (buf, string, offset, length) {
	  return asciiWrite(buf, string, offset, length)
    }

    function base64Write (buf, string, offset, length) {
	  return blitBuffer(base64ToBytes(string), buf, offset, length)
    }

    function ucs2Write (buf, string, offset, length) {
	  return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length)
    }

    Buffer.prototype.write = function write (string, offset, length, encoding) {
	  // Buffer#write(string)
	  if (offset === undefined) {
	    encoding = 'utf8'
	    length = this.length
	    offset = 0
	  // Buffer#write(string, encoding)
	  } else if (length === undefined && typeof offset === 'string') {
	    encoding = offset
	    length = this.length
	    offset = 0
	  // Buffer#write(string, offset[, length][, encoding])
	  } else if (isFinite(offset)) {
	    offset = offset | 0
	    if (isFinite(length)) {
	      length = length | 0
	      if (encoding === undefined) encoding = 'utf8'
	    } else {
	      encoding = length
	      length = undefined
	    }
	  // legacy write(string, encoding, offset, length) - remove in v0.13
	  } else {
	    var swap = encoding
	    encoding = offset
	    offset = length | 0
	    length = swap
	  }

	  var remaining = this.length - offset
	  if (length === undefined || length > remaining) length = remaining

	  if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) {
	    throw new RangeError('attempt to write outside buffer bounds')
	  }

	  if (!encoding) encoding = 'utf8'

	  var loweredCase = false
	  for (;;) {
	    switch (encoding) {
	      case 'hex':
	        return hexWrite(this, string, offset, length)

	      case 'utf8':
	      case 'utf-8':
	        return utf8Write(this, string, offset, length)

	      case 'ascii':
	        return asciiWrite(this, string, offset, length)

	      case 'binary':
	        return binaryWrite(this, string, offset, length)

	      case 'base64':
	        // Warning: maxLength not taken into account in base64Write
	        return base64Write(this, string, offset, length)

	      case 'ucs2':
	      case 'ucs-2':
	      case 'utf16le':
	      case 'utf-16le':
	        return ucs2Write(this, string, offset, length)

	      default:
	        if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
	        encoding = ('' + encoding).toLowerCase()
	        loweredCase = true
	    }
	  }
    }

    Buffer.prototype.toJSON = function toJSON () {
	  return {
	    type: 'Buffer',
	    data: Array.prototype.slice.call(this._arr || this, 0)
	  }
    }

    function base64Slice (buf, start, end) {
	  if (start === 0 && end === buf.length) {
	    return base64.fromByteArray(buf)
	  } else {
	    return base64.fromByteArray(buf.slice(start, end))
	  }
    }

    function utf8Slice (buf, start, end) {
	  end = Math.min(buf.length, end)
	  var res = []

	  var i = start
	  while (i < end) {
	    var firstByte = buf[i]
	    var codePoint = null
	    var bytesPerSequence = (firstByte > 0xEF) ? 4
	      : (firstByte > 0xDF) ? 3
	      : (firstByte > 0xBF) ? 2
	      : 1

	    if (i + bytesPerSequence <= end) {
	      var secondByte, thirdByte, fourthByte, tempCodePoint

	      switch (bytesPerSequence) {
	        case 1:
	          if (firstByte < 0x80) {
	            codePoint = firstByte
	          }
	          break
	        case 2:
	          secondByte = buf[i + 1]
	          if ((secondByte & 0xC0) === 0x80) {
	            tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F)
	            if (tempCodePoint > 0x7F) {
	              codePoint = tempCodePoint
	            }
	          }
	          break
	        case 3:
	          secondByte = buf[i + 1]
	          thirdByte = buf[i + 2]
	          if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) {
	            tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F)
	            if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) {
	              codePoint = tempCodePoint
	            }
	          }
	          break
	        case 4:
	          secondByte = buf[i + 1]
	          thirdByte = buf[i + 2]
	          fourthByte = buf[i + 3]
	          if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) {
	            tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F)
	            if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) {
	              codePoint = tempCodePoint
	            }
	          }
	      }
	    }

	    if (codePoint === null) {
	      // we did not generate a valid codePoint so insert a
	      // replacement char (U+FFFD) and advance only 1 byte
	      codePoint = 0xFFFD
	      bytesPerSequence = 1
	    } else if (codePoint > 0xFFFF) {
	      // encode to utf16 (surrogate pair dance)
	      codePoint -= 0x10000
	      res.push(codePoint >>> 10 & 0x3FF | 0xD800)
	      codePoint = 0xDC00 | codePoint & 0x3FF
	    }

	    res.push(codePoint)
	    i += bytesPerSequence
	  }

	  return decodeCodePointsArray(res)
    }

    // Based on http://stackoverflow.com/a/22747272/680742, the browser with
    // the lowest limit is Chrome, with 0x10000 args.
    // We go 1 magnitude less, for safety
    var MAX_ARGUMENTS_LENGTH = 0x1000

    function decodeCodePointsArray (codePoints) {
	  var len = codePoints.length
	  if (len <= MAX_ARGUMENTS_LENGTH) {
	    return String.fromCharCode.apply(String, codePoints) // avoid extra slice()
	  }

	  // Decode in chunks to avoid "call stack size exceeded".
	  var res = ''
	  var i = 0
	  while (i < len) {
	    res += String.fromCharCode.apply(
	      String,
	      codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH)
	    )
	  }
	  return res
    }

    function asciiSlice (buf, start, end) {
	  var ret = ''
	  end = Math.min(buf.length, end)

	  for (var i = start; i < end; i++) {
	    ret += String.fromCharCode(buf[i] & 0x7F)
	  }
	  return ret
    }

    function binarySlice (buf, start, end) {
	  var ret = ''
	  end = Math.min(buf.length, end)

	  for (var i = start; i < end; i++) {
	    ret += String.fromCharCode(buf[i])
	  }
	  return ret
    }

    function hexSlice (buf, start, end) {
	  var len = buf.length

	  if (!start || start < 0) start = 0
	  if (!end || end < 0 || end > len) end = len

	  var out = ''
	  for (var i = start; i < end; i++) {
	    out += toHex(buf[i])
	  }
	  return out
    }

    function utf16leSlice (buf, start, end) {
	  var bytes = buf.slice(start, end)
	  var res = ''
	  for (var i = 0; i < bytes.length; i += 2) {
	    res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256)
	  }
	  return res
    }

    Buffer.prototype.slice = function slice (start, end) {
	  var len = this.length
	  start = ~~start
	  end = end === undefined ? len : ~~end

	  if (start < 0) {
	    start += len
	    if (start < 0) start = 0
	  } else if (start > len) {
	    start = len
	  }

	  if (end < 0) {
	    end += len
	    if (end < 0) end = 0
	  } else if (end > len) {
	    end = len
	  }

	  if (end < start) end = start

	  var newBuf
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    newBuf = this.subarray(start, end)
	    newBuf.__proto__ = Buffer.prototype
	  } else {
	    var sliceLen = end - start
	    newBuf = new Buffer(sliceLen, undefined)
	    for (var i = 0; i < sliceLen; i++) {
	      newBuf[i] = this[i + start]
	    }
	  }

	  if (newBuf.length) newBuf.parent = this.parent || this

	  return newBuf
    }

    /*
	 * Need to make sure that buffer isn't trying to write out of bounds.
	 */
    function checkOffset (offset, ext, length) {
	  if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint')
	  if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length')
    }

    Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) {
	  offset = offset | 0
	  byteLength = byteLength | 0
	  if (!noAssert) checkOffset(offset, byteLength, this.length)

	  var val = this[offset]
	  var mul = 1
	  var i = 0
	  while (++i < byteLength && (mul *= 0x100)) {
	    val += this[offset + i] * mul
	  }

	  return val
    }

    Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) {
	  offset = offset | 0
	  byteLength = byteLength | 0
	  if (!noAssert) {
	    checkOffset(offset, byteLength, this.length)
	  }

	  var val = this[offset + --byteLength]
	  var mul = 1
	  while (byteLength > 0 && (mul *= 0x100)) {
	    val += this[offset + --byteLength] * mul
	  }

	  return val
    }

    Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 1, this.length)
	  return this[offset]
    }

    Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 2, this.length)
	  return this[offset] | (this[offset + 1] << 8)
    }

    Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 2, this.length)
	  return (this[offset] << 8) | this[offset + 1]
    }

    Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 4, this.length)

	  return ((this[offset]) |
	      (this[offset + 1] << 8) |
	      (this[offset + 2] << 16)) +
	      (this[offset + 3] * 0x1000000)
    }

    Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 4, this.length)

	  return (this[offset] * 0x1000000) +
	    ((this[offset + 1] << 16) |
	    (this[offset + 2] << 8) |
	    this[offset + 3])
    }

    Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) {
	  offset = offset | 0
	  byteLength = byteLength | 0
	  if (!noAssert) checkOffset(offset, byteLength, this.length)

	  var val = this[offset]
	  var mul = 1
	  var i = 0
	  while (++i < byteLength && (mul *= 0x100)) {
	    val += this[offset + i] * mul
	  }
	  mul *= 0x80

	  if (val >= mul) val -= Math.pow(2, 8 * byteLength)

	  return val
    }

    Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) {
	  offset = offset | 0
	  byteLength = byteLength | 0
	  if (!noAssert) checkOffset(offset, byteLength, this.length)

	  var i = byteLength
	  var mul = 1
	  var val = this[offset + --i]
	  while (i > 0 && (mul *= 0x100)) {
	    val += this[offset + --i] * mul
	  }
	  mul *= 0x80

	  if (val >= mul) val -= Math.pow(2, 8 * byteLength)

	  return val
    }

    Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 1, this.length)
	  if (!(this[offset] & 0x80)) return (this[offset])
	  return ((0xff - this[offset] + 1) * -1)
    }

    Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 2, this.length)
	  var val = this[offset] | (this[offset + 1] << 8)
	  return (val & 0x8000) ? val | 0xFFFF0000 : val
    }

    Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 2, this.length)
	  var val = this[offset + 1] | (this[offset] << 8)
	  return (val & 0x8000) ? val | 0xFFFF0000 : val
    }

    Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 4, this.length)

	  return (this[offset]) |
	    (this[offset + 1] << 8) |
	    (this[offset + 2] << 16) |
	    (this[offset + 3] << 24)
    }

    Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 4, this.length)

	  return (this[offset] << 24) |
	    (this[offset + 1] << 16) |
	    (this[offset + 2] << 8) |
	    (this[offset + 3])
    }

    Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 4, this.length)
	  return ieee754.read(this, offset, true, 23, 4)
    }

    Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 4, this.length)
	  return ieee754.read(this, offset, false, 23, 4)
    }

    Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 8, this.length)
	  return ieee754.read(this, offset, true, 52, 8)
    }

    Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) {
	  if (!noAssert) checkOffset(offset, 8, this.length)
	  return ieee754.read(this, offset, false, 52, 8)
    }

    function checkInt (buf, value, offset, ext, max, min) {
	  if (!Buffer.isBuffer(buf)) throw new TypeError('buffer must be a Buffer instance')
	  if (value > max || value < min) throw new RangeError('value is out of bounds')
	  if (offset + ext > buf.length) throw new RangeError('index out of range')
    }

    Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) {
	  value = +value
	  offset = offset | 0
	  byteLength = byteLength | 0
	  if (!noAssert) checkInt(this, value, offset, byteLength, Math.pow(2, 8 * byteLength), 0)

	  var mul = 1
	  var i = 0
	  this[offset] = value & 0xFF
	  while (++i < byteLength && (mul *= 0x100)) {
	    this[offset + i] = (value / mul) & 0xFF
	  }

	  return offset + byteLength
    }

    Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) {
	  value = +value
	  offset = offset | 0
	  byteLength = byteLength | 0
	  if (!noAssert) checkInt(this, value, offset, byteLength, Math.pow(2, 8 * byteLength), 0)

	  var i = byteLength - 1
	  var mul = 1
	  this[offset + i] = value & 0xFF
	  while (--i >= 0 && (mul *= 0x100)) {
	    this[offset + i] = (value / mul) & 0xFF
	  }

	  return offset + byteLength
    }

    Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0)
	  if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
	  this[offset] = (value & 0xff)
	  return offset + 1
    }

    function objectWriteUInt16 (buf, value, offset, littleEndian) {
	  if (value < 0) value = 0xffff + value + 1
	  for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; i++) {
	    buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>>
	      (littleEndian ? i : 1 - i) * 8
	  }
    }

    Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset] = (value & 0xff)
	    this[offset + 1] = (value >>> 8)
	  } else {
	    objectWriteUInt16(this, value, offset, true)
	  }
	  return offset + 2
    }

    Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset] = (value >>> 8)
	    this[offset + 1] = (value & 0xff)
	  } else {
	    objectWriteUInt16(this, value, offset, false)
	  }
	  return offset + 2
    }

    function objectWriteUInt32 (buf, value, offset, littleEndian) {
	  if (value < 0) value = 0xffffffff + value + 1
	  for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; i++) {
	    buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff
	  }
    }

    Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset + 3] = (value >>> 24)
	    this[offset + 2] = (value >>> 16)
	    this[offset + 1] = (value >>> 8)
	    this[offset] = (value & 0xff)
	  } else {
	    objectWriteUInt32(this, value, offset, true)
	  }
	  return offset + 4
    }

    Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset] = (value >>> 24)
	    this[offset + 1] = (value >>> 16)
	    this[offset + 2] = (value >>> 8)
	    this[offset + 3] = (value & 0xff)
	  } else {
	    objectWriteUInt32(this, value, offset, false)
	  }
	  return offset + 4
    }

    Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) {
	    var limit = Math.pow(2, 8 * byteLength - 1)

	    checkInt(this, value, offset, byteLength, limit - 1, -limit)
	  }

	  var i = 0
	  var mul = 1
	  var sub = value < 0 ? 1 : 0
	  this[offset] = value & 0xFF
	  while (++i < byteLength && (mul *= 0x100)) {
	    this[offset + i] = ((value / mul) >> 0) - sub & 0xFF
	  }

	  return offset + byteLength
    }

    Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) {
	    var limit = Math.pow(2, 8 * byteLength - 1)

	    checkInt(this, value, offset, byteLength, limit - 1, -limit)
	  }

	  var i = byteLength - 1
	  var mul = 1
	  var sub = value < 0 ? 1 : 0
	  this[offset + i] = value & 0xFF
	  while (--i >= 0 && (mul *= 0x100)) {
	    this[offset + i] = ((value / mul) >> 0) - sub & 0xFF
	  }

	  return offset + byteLength
    }

    Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80)
	  if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
	  if (value < 0) value = 0xff + value + 1
	  this[offset] = (value & 0xff)
	  return offset + 1
    }

    Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset] = (value & 0xff)
	    this[offset + 1] = (value >>> 8)
	  } else {
	    objectWriteUInt16(this, value, offset, true)
	  }
	  return offset + 2
    }

    Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset] = (value >>> 8)
	    this[offset + 1] = (value & 0xff)
	  } else {
	    objectWriteUInt16(this, value, offset, false)
	  }
	  return offset + 2
    }

    Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset] = (value & 0xff)
	    this[offset + 1] = (value >>> 8)
	    this[offset + 2] = (value >>> 16)
	    this[offset + 3] = (value >>> 24)
	  } else {
	    objectWriteUInt32(this, value, offset, true)
	  }
	  return offset + 4
    }

    Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) {
	  value = +value
	  offset = offset | 0
	  if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
	  if (value < 0) value = 0xffffffff + value + 1
	  if (Buffer.TYPED_ARRAY_SUPPORT) {
	    this[offset] = (value >>> 24)
	    this[offset + 1] = (value >>> 16)
	    this[offset + 2] = (value >>> 8)
	    this[offset + 3] = (value & 0xff)
	  } else {
	    objectWriteUInt32(this, value, offset, false)
	  }
	  return offset + 4
    }

    function checkIEEE754 (buf, value, offset, ext, max, min) {
	  if (value > max || value < min) throw new RangeError('value is out of bounds')
	  if (offset + ext > buf.length) throw new RangeError('index out of range')
	  if (offset < 0) throw new RangeError('index out of range')
    }

    function writeFloat (buf, value, offset, littleEndian, noAssert) {
	  if (!noAssert) {
	    checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38)
	  }
	  ieee754.write(buf, value, offset, littleEndian, 23, 4)
	  return offset + 4
    }

    Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) {
	  return writeFloat(this, value, offset, true, noAssert)
    }

    Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) {
	  return writeFloat(this, value, offset, false, noAssert)
    }

    function writeDouble (buf, value, offset, littleEndian, noAssert) {
	  if (!noAssert) {
	    checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308)
	  }
	  ieee754.write(buf, value, offset, littleEndian, 52, 8)
	  return offset + 8
    }

    Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) {
	  return writeDouble(this, value, offset, true, noAssert)
    }

    Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) {
	  return writeDouble(this, value, offset, false, noAssert)
    }

    // copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
    Buffer.prototype.copy = function copy (target, targetStart, start, end) {
	  if (!start) start = 0
	  if (!end && end !== 0) end = this.length
	  if (targetStart >= target.length) targetStart = target.length
	  if (!targetStart) targetStart = 0
	  if (end > 0 && end < start) end = start

	  // Copy 0 bytes; we're done
	  if (end === start) return 0
	  if (target.length === 0 || this.length === 0) return 0

	  // Fatal error conditions
	  if (targetStart < 0) {
	    throw new RangeError('targetStart out of bounds')
	  }
	  if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds')
	  if (end < 0) throw new RangeError('sourceEnd out of bounds')

	  // Are we oob?
	  if (end > this.length) end = this.length
	  if (target.length - targetStart < end - start) {
	    end = target.length - targetStart + start
	  }

	  var len = end - start
	  var i

	  if (this === target && start < targetStart && targetStart < end) {
	    // descending copy from end
	    for (i = len - 1; i >= 0; i--) {
	      target[i + targetStart] = this[i + start]
	    }
	  } else if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) {
	    // ascending copy from start
	    for (i = 0; i < len; i++) {
	      target[i + targetStart] = this[i + start]
	    }
	  } else {
	    Uint8Array.prototype.set.call(
	      target,
	      this.subarray(start, start + len),
	      targetStart
	    )
	  }

	  return len
    }

    // fill(value, start=0, end=buffer.length)
    Buffer.prototype.fill = function fill (value, start, end) {
	  if (!value) value = 0
	  if (!start) start = 0
	  if (!end) end = this.length

	  if (end < start) throw new RangeError('end < start')

	  // Fill 0 bytes; we're done
	  if (end === start) return
	  if (this.length === 0) return

	  if (start < 0 || start >= this.length) throw new RangeError('start out of bounds')
	  if (end < 0 || end > this.length) throw new RangeError('end out of bounds')

	  var i
	  if (typeof value === 'number') {
	    for (i = start; i < end; i++) {
	      this[i] = value
	    }
	  } else {
	    var bytes = utf8ToBytes(value.toString())
	    var len = bytes.length
	    for (i = start; i < end; i++) {
	      this[i] = bytes[i % len]
	    }
	  }

	  return this
    }

    // HELPER FUNCTIONS
    // ================

    var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g

    function base64clean (str) {
	  // Node strips out invalid characters like \n and \t from the string, base64-js does not
	  str = stringtrim(str).replace(INVALID_BASE64_RE, '')
	  // Node converts strings with length < 2 to ''
	  if (str.length < 2) return ''
	  // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
	  while (str.length % 4 !== 0) {
	    str = str + '='
	  }
	  return str
    }

    function stringtrim (str) {
	  if (str.trim) return str.trim()
	  return str.replace(/^\s+|\s+$/g, '')
    }

    function toHex (n) {
	  if (n < 16) return '0' + n.toString(16)
	  return n.toString(16)
    }

    function utf8ToBytes (string, units) {
	  units = units || Infinity
	  var codePoint
	  var length = string.length
	  var leadSurrogate = null
	  var bytes = []

	  for (var i = 0; i < length; i++) {
	    codePoint = string.charCodeAt(i)

	    // is surrogate component
	    if (codePoint > 0xD7FF && codePoint < 0xE000) {
	      // last char was a lead
	      if (!leadSurrogate) {
	        // no lead yet
	        if (codePoint > 0xDBFF) {
	          // unexpected trail
	          if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
	          continue
	        } else if (i + 1 === length) {
	          // unpaired lead
	          if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
	          continue
	        }

	        // valid lead
	        leadSurrogate = codePoint

	        continue
	      }

	      // 2 leads in a row
	      if (codePoint < 0xDC00) {
	        if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
	        leadSurrogate = codePoint
	        continue
	      }

	      // valid surrogate pair
	      codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000
	    } else if (leadSurrogate) {
	      // valid bmp char, but last char was a lead
	      if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
	    }

	    leadSurrogate = null

	    // encode utf8
	    if (codePoint < 0x80) {
	      if ((units -= 1) < 0) break
	      bytes.push(codePoint)
	    } else if (codePoint < 0x800) {
	      if ((units -= 2) < 0) break
	      bytes.push(
	        codePoint >> 0x6 | 0xC0,
	        codePoint & 0x3F | 0x80
	      )
	    } else if (codePoint < 0x10000) {
	      if ((units -= 3) < 0) break
	      bytes.push(
	        codePoint >> 0xC | 0xE0,
	        codePoint >> 0x6 & 0x3F | 0x80,
	        codePoint & 0x3F | 0x80
	      )
	    } else if (codePoint < 0x110000) {
	      if ((units -= 4) < 0) break
	      bytes.push(
	        codePoint >> 0x12 | 0xF0,
	        codePoint >> 0xC & 0x3F | 0x80,
	        codePoint >> 0x6 & 0x3F | 0x80,
	        codePoint & 0x3F | 0x80
	      )
	    } else {
	      throw new Error('Invalid code point')
	    }
	  }

	  return bytes
    }

    function asciiToBytes (str) {
	  var byteArray = []
	  for (var i = 0; i < str.length; i++) {
	    // Node's code seems to be doing this and not & 0x7F..
	    byteArray.push(str.charCodeAt(i) & 0xFF)
	  }
	  return byteArray
    }

    function utf16leToBytes (str, units) {
	  var c, hi, lo
	  var byteArray = []
	  for (var i = 0; i < str.length; i++) {
	    if ((units -= 2) < 0) break

	    c = str.charCodeAt(i)
	    hi = c >> 8
	    lo = c % 256
	    byteArray.push(lo)
	    byteArray.push(hi)
	  }

	  return byteArray
    }

    function base64ToBytes (str) {
	  return base64.toByteArray(base64clean(str))
    }

    function blitBuffer (src, dst, offset, length) {
	  for (var i = 0; i < length; i++) {
	    if ((i + offset >= dst.length) || (i >= src.length)) break
	    dst[i + offset] = src[i]
	  }
	  return i
    }
  }).call(this, typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {})
}, {'base64-js': 38, 'ieee754': 39, 'isarray': 40}],
38: [function (require, module, exports) {
  (function (exports) {
	  'use strict'

	  var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

	  var Arr = (typeof Uint8Array !== 'undefined')
	    ? Uint8Array
	    : Array

	  var PLUS = '+'.charCodeAt(0)
	  var SLASH = '/'.charCodeAt(0)
	  var NUMBER = '0'.charCodeAt(0)
	  var LOWER = 'a'.charCodeAt(0)
	  var UPPER = 'A'.charCodeAt(0)
	  var PLUS_URL_SAFE = '-'.charCodeAt(0)
	  var SLASH_URL_SAFE = '_'.charCodeAt(0)

	  function decode (elt) {
	    var code = elt.charCodeAt(0)
	    if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
	    if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
	    if (code < NUMBER) return -1 // no match
	    if (code < NUMBER + 10) return code - NUMBER + 26 + 26
	    if (code < UPPER + 26) return code - UPPER
	    if (code < LOWER + 26) return code - LOWER + 26
	  }

	  function b64ToByteArray (b64) {
	    var i, j, l, tmp, placeHolders, arr

	    if (b64.length % 4 > 0) {
	      throw new Error('Invalid string. Length must be a multiple of 4')
	    }

	    // the number of equal signs (place holders)
	    // if there are two placeholders, than the two characters before it
	    // represent one byte
	    // if there is only one, then the three characters before it represent 2 bytes
	    // this is just a cheap hack to not do indexOf twice
	    var len = b64.length
	    placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0

	    // base64 is 4/3 + up to two characters of the original data
	    arr = new Arr(b64.length * 3 / 4 - placeHolders)

	    // if there are placeholders, only get up to the last complete 4 chars
	    l = placeHolders > 0 ? b64.length - 4 : b64.length

	    var L = 0

	    function push (v) {
	      arr[L++] = v
	    }

	    for (i = 0, j = 0; i < l; i += 4, j += 3) {
	      tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
	      push((tmp & 0xFF0000) >> 16)
	      push((tmp & 0xFF00) >> 8)
	      push(tmp & 0xFF)
	    }

	    if (placeHolders === 2) {
	      tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
	      push(tmp & 0xFF)
	    } else if (placeHolders === 1) {
	      tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
	      push((tmp >> 8) & 0xFF)
	      push(tmp & 0xFF)
	    }

	    return arr
	  }

	  function uint8ToBase64 (uint8) {
	    var i
	    var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
	    var output = ''
	    var temp, length

	    function encode (num) {
	      return lookup.charAt(num)
	    }

	    function tripletToBase64 (num) {
	      return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
	    }

	    // go through the array every three bytes, we'll deal with trailing stuff later
	    for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
	      temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
	      output += tripletToBase64(temp)
	    }

	    // pad the end with zeros, but make sure to not forget the extra bytes
	    switch (extraBytes) {
	      case 1:
	        temp = uint8[uint8.length - 1]
	        output += encode(temp >> 2)
	        output += encode((temp << 4) & 0x3F)
	        output += '=='
	        break
	      case 2:
	        temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
	        output += encode(temp >> 10)
	        output += encode((temp >> 4) & 0x3F)
	        output += encode((temp << 2) & 0x3F)
	        output += '='
	        break
	      default:
	        break
	    }

	    return output
	  }

	  exports.toByteArray = b64ToByteArray
	  exports.fromByteArray = uint8ToBase64
  }(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
}, {}],
39: [function (require, module, exports) {
  exports.read = function (buffer, offset, isLE, mLen, nBytes) {
	  var e, m
	  var eLen = nBytes * 8 - mLen - 1
	  var eMax = (1 << eLen) - 1
	  var eBias = eMax >> 1
	  var nBits = -7
	  var i = isLE ? (nBytes - 1) : 0
	  var d = isLE ? -1 : 1
	  var s = buffer[offset + i]

	  i += d

	  e = s & ((1 << (-nBits)) - 1)
	  s >>= (-nBits)
	  nBits += eLen
	  for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}

	  m = e & ((1 << (-nBits)) - 1)
	  e >>= (-nBits)
	  nBits += mLen
	  for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}

	  if (e === 0) {
	    e = 1 - eBias
	  } else if (e === eMax) {
	    return m ? NaN : ((s ? -1 : 1) * Infinity)
	  } else {
	    m = m + Math.pow(2, mLen)
	    e = e - eBias
	  }
	  return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
  }

  exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
	  var e, m, c
	  var eLen = nBytes * 8 - mLen - 1
	  var eMax = (1 << eLen) - 1
	  var eBias = eMax >> 1
	  var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
	  var i = isLE ? 0 : (nBytes - 1)
	  var d = isLE ? 1 : -1
	  var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0

	  value = Math.abs(value)

	  if (isNaN(value) || value === Infinity) {
	    m = isNaN(value) ? 1 : 0
	    e = eMax
	  } else {
	    e = Math.floor(Math.log(value) / Math.LN2)
	    if (value * (c = Math.pow(2, -e)) < 1) {
	      e--
	      c *= 2
	    }
	    if (e + eBias >= 1) {
	      value += rt / c
	    } else {
	      value += rt * Math.pow(2, 1 - eBias)
	    }
	    if (value * c >= 2) {
	      e++
	      c /= 2
	    }

	    if (e + eBias >= eMax) {
	      m = 0
	      e = eMax
	    } else if (e + eBias >= 1) {
	      m = (value * c - 1) * Math.pow(2, mLen)
	      e = e + eBias
	    } else {
	      m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
	      e = 0
	    }
	  }

	  for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}

	  e = (e << mLen) | m
	  eLen += mLen
	  for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}

	  buffer[offset + i - d] |= s * 128
  }
}, {}],
40: [function (require, module, exports) {
  var toString = {}.toString

  module.exports = Array.isArray || function (arr) {
	  return toString.call(arr) == '[object Array]'
  }
}, {}]}, {}, [36])

export default PANOLENS
